diff --git a/Cargo.lock b/Cargo.lock index b034b863..4c1ac078 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3154,18 +3154,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "race-example-draw-card" -version = "0.1.0" -dependencies = [ - "anyhow", - "arrayref", - "borsh 1.5.1", - "race-api", - "race-proc-macro", - "race-test", -] - [[package]] name = "race-example-minimal" version = "0.1.0" @@ -3176,16 +3164,6 @@ dependencies = [ "race-test", ] -[[package]] -name = "race-example-raffle" -version = "0.1.0" -dependencies = [ - "borsh 1.5.1", - "race-api", - "race-proc-macro", - "race-test", -] - [[package]] name = "race-facade" version = "0.2.6" diff --git a/Cargo.toml b/Cargo.toml index 86ce8f60..71eac88b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,8 +15,8 @@ members = [ "test", "local-db", "examples/minimal", - "examples/draw-card", - "examples/raffle", + # "examples/draw-card", + # "examples/raffle", # "examples/simple-settle", # "examples/blackjack", # "examples/roshambo", diff --git a/Justfile b/Justfile index 7740630e..cceffa0c 100644 --- a/Justfile +++ b/Justfile @@ -1,31 +1,34 @@ set dotenv-load +# Release build transactor and command line tool build: build-transactor build-cli +# Install NPM / Cargo dependencies dep: cargo fetch npm --prefix ./js i -ws npm --prefix ./examples/demo-app i +# Release build facade server build-facade: cargo build -r -p race-facade +# Release build transactor server build-transactor: cargo build -r -p race-transactor +# Release build command line tool build-cli: cargo build -r -p race-cli +# Call command line tool, use `just cli help` to show help menu cli *ARGS: cargo run -p race-cli -- {{ARGS}} -test: test-core test-transactor - -test-transactor: - cargo test -p race-transactor - -test-core: - cargo test -p race-core +# Run cargo test +test: + cargo test + npm test --prefix ./js examples: example-chat example-raffle @@ -58,15 +61,19 @@ example-draw-card: mkdir -p dev/dist cp target/race_example_draw_card.wasm dev/dist/ +# Start demo app which serves the games in `examples` dev-demo-app: npm --prefix ./examples/demo-app run dev +# Release build the demo-app build-demo-app: npm --prefix ./examples/demo-app run build +# Release build the demo-app and open it in browser preview-demo-app: build-demo-app npm --prefix ./examples/demo-app run preview +# Run facade with dev build, use `--help` to show help menu dev-facade *ARGS: cargo run -p race-facade -- {{ARGS}} @@ -76,15 +83,10 @@ dev-reg-transactor conf: dev-run-transactor conf: cargo run -p race-transactor -- -c {{conf}} run +# Start transactor dev build, read CONF configuration, register and run dev-transactor conf: (dev-reg-transactor conf) (dev-run-transactor conf) -solana: - (cd contracts/solana; cargo build-sbf) - -solana-local: solana - solana program deploy ./target/deploy/race_solana.so - -borsh: +sdk-borsh: npm --prefix ./js/borsh run build sdk-core: @@ -96,29 +98,22 @@ sdk-solana: sdk-facade: npm --prefix ./js/sdk-facade run build -sdk: borsh sdk-core sdk-solana sdk-facade - -publish name url: - cargo run -p race-cli -- -e local publish solana {{name}} {{url}} - -create-reg: - cargo run -p race-cli -- -e local create-reg solana - -create-game spec: - cargo run -p race-cli -- -e local create-game solana {{spec}} - -validator: - solana-test-validator --bpf-program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s token_metadata_program.so +# Build all libs under js folder +sdk: sdk-borsh sdk-core sdk-solana sdk-facade +# Publish js PKG to npmjs publish-npmjs pkg: npm --prefix ./js/{{pkg}} run build (cd js/{{pkg}}; npm publish --access=public) +# Publish all js pacakges publish-npmjs-all: (publish-npmjs "borsh") (publish-npmjs "sdk-core") (publish-npmjs "sdk-facade") (publish-npmjs "sdk-solana") +# Publish rust PKG to crates.io publish-crates pkg: cargo check -p {{pkg}} cargo test -p {{pkg}} cargo publish -p {{pkg}} +# Publish all rust packages to crates.io publish-crates-all: (publish-crates "race-api") (publish-crates "race-proc-macro") (publish-crates "race-core") (publish-crates "race-encryptor") (publish-crates "race-client") (publish-crates "race-test") diff --git a/api/src/effect.rs b/api/src/effect.rs index dd93471e..3154f5f3 100644 --- a/api/src/effect.rs +++ b/api/src/effect.rs @@ -22,13 +22,13 @@ pub struct Ask { pub struct Assign { pub random_id: RandomId, pub player_id: u64, - pub indexes: Vec, + pub indices: Vec, } #[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Eq)] pub struct Reveal { pub random_id: RandomId, - pub indexes: Vec, + pub indices: Vec, } #[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Eq)] @@ -68,7 +68,6 @@ impl SubGame { bundle_addr: String, max_players: u16, init_data: S, - checkpoint_state: T, ) -> Result { Ok(Self { id, @@ -76,7 +75,6 @@ impl SubGame { init_account: InitAccount { max_players, data: borsh::to_vec(&init_data)?, - checkpoint: Some(borsh::to_vec(&checkpoint_state)?), }, }) } @@ -132,7 +130,7 @@ impl EmitBridgeEvent { /// ``` /// # use race_api::effect::Effect; /// let mut effect = Effect::default(); -/// effect.assign(1 /* random_id */, 0 /* player_id */, vec![0, 1, 2] /* indexes */); +/// effect.assign(1 /* random_id */, 0 /* player_id */, vec![0, 1, 2] /* indices */); /// ``` /// To reveal some items to the public, use [`Effect::reveal`]. /// It makes those items visible to everyone, including servers. @@ -140,7 +138,7 @@ impl EmitBridgeEvent { /// ``` /// # use race_api::effect::Effect; /// let mut effect = Effect::default(); -/// effect.reveal(1 /* random_id */, vec![0, 1, 2] /* indexes */); +/// effect.reveal(1 /* random_id */, vec![0, 1, 2] /* indices */); /// ``` /// /// # Decisions @@ -183,13 +181,7 @@ impl EmitBridgeEvent { /// # use race_api::effect::Effect; /// use race_api::types::Settle; /// let mut effect = Effect::default(); -/// // Increase assets -/// effect.settle(Settle::add(0 /* player_id */, 100 /* amount */)); -/// // Decrease assets -/// effect.settle(Settle::sub(1 /* player_id */, 200 /* amount */)); -/// // Remove player from this game, its assets will be paid out -/// effect.settle(Settle::eject(2 /* player_id*/)); -/// // Make the checkpoint +/// effect.settle(0 /* player_id */, 100 /* amount */); /// effect.checkpoint(); /// ``` @@ -259,20 +251,20 @@ impl Effect { &mut self, random_id: RandomId, player_id: u64, - indexes: Vec, + indices: Vec, ) -> Result<()> { self.assert_player_id(player_id, "assign random id")?; self.assigns.push(Assign { random_id, player_id, - indexes, + indices, }); Ok(()) } /// Reveal some random items to the public. - pub fn reveal(&mut self, random_id: RandomId, indexes: Vec) { - self.reveals.push(Reveal { random_id, indexes }) + pub fn reveal(&mut self, random_id: RandomId, indices: Vec) { + self.reveals.push(Reveal { random_id, indices }) } /// Return the revealed random items by id. @@ -388,9 +380,6 @@ impl Effect { init_account: InitAccount { max_players, data: borsh::to_vec(&init_data)?, - // The checkpoint is always None - // It represents no checkpoint or we should use the existing one - checkpoint: None, }, }); Ok(()) diff --git a/api/src/engine.rs b/api/src/engine.rs index 55b6ca6e..2245a1c9 100644 --- a/api/src/engine.rs +++ b/api/src/engine.rs @@ -3,9 +3,8 @@ use borsh::{BorshDeserialize, BorshSerialize}; use crate::{effect::Effect, error::HandleResult, event::Event, init_account::InitAccount}; pub trait GameHandler: Sized + BorshSerialize + BorshDeserialize { - /// 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; + /// Initialize handler state with on-chain game account data. + fn init_state(init_account: InitAccount) -> HandleResult; /// Handle event. fn handle_event(&mut self, effect: &mut Effect, event: Event) -> HandleResult<()>; diff --git a/api/src/event.rs b/api/src/event.rs index 2bb25fdb..36c2b910 100644 --- a/api/src/event.rs +++ b/api/src/event.rs @@ -98,11 +98,11 @@ pub enum Event { /// Timeout when waiting for start WaitingTimeout, - /// Random drawer takes random items by indexes. + /// Random drawer takes random items by indices. DrawRandomItems { sender: u64, random_id: usize, - indexes: Vec, + indices: Vec, }, /// Timeout for drawing random items @@ -203,11 +203,11 @@ impl std::fmt::Display for Event { Event::DrawRandomItems { sender, random_id, - indexes, + indices, } => write!( f, - "DrawRandomItems from {} for random {} with indexes {:?}", - sender, random_id, indexes + "DrawRandomItems from {} for random {} with indices {:?}", + sender, random_id, indices ), Event::DrawTimeout => write!(f, "DrawTimeout"), Event::ActionTimeout { player_id } => write!(f, "ActionTimeout for {}", player_id), diff --git a/api/src/init_account.rs b/api/src/init_account.rs index aad7eea1..14bfdd5c 100644 --- a/api/src/init_account.rs +++ b/api/src/init_account.rs @@ -1,29 +1,18 @@ use borsh::{BorshDeserialize, BorshSerialize}; -use crate::{ - error::{HandleError, HandleResult}, -}; +use crate::error::HandleError; /// A set of arguments for game handler initialization. #[derive(Debug, Clone, BorshSerialize, BorshDeserialize, PartialEq, Eq)] pub struct InitAccount { pub max_players: u16, pub data: Vec, - pub checkpoint: Option>, } impl InitAccount { pub fn data(&self) -> Result { S::try_from_slice(&self.data).or(Err(HandleError::MalformedGameAccountData)) } - - /// Get deserialized checkpoint, return None if not available. - pub fn checkpoint(&self) -> HandleResult> { - self.checkpoint - .as_ref() - .map(|c| T::try_from_slice(c).map_err(|_| HandleError::MalformedCheckpointData)) - .transpose() - } } impl Default for InitAccount { @@ -31,7 +20,6 @@ impl Default for InitAccount { Self { max_players: 0, data: Vec::new(), - checkpoint: None, } } } diff --git a/client/src/lib.rs b/client/src/lib.rs index 89c78198..4bece190 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -351,7 +351,7 @@ impl Client { } /// Decrypt the ciphertexts with shared secrets. - /// Return a mapping from mapping from indexes to decrypted value. + /// Return a mapping from mapping from indices to decrypted value. pub fn decrypt( &self, ctx: &GameContext, diff --git a/core/src/context.rs b/core/src/context.rs index cae17147..6244614a 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -264,21 +264,26 @@ impl GameContext { )) }); - let checkpoint = match ( + let (checkpoint, handler_state, state_sha) = match ( checkpoint_off_chain, game_account.checkpoint_on_chain.as_ref(), ) { (Some(off_chain), Some(on_chain)) => { - Checkpoint::new_from_parts(off_chain, on_chain.clone()) + let checkpoint = Checkpoint::new_from_parts(off_chain, on_chain.clone()); + let Some(ref handler_state) = checkpoint.get_data(0) else { + return Err(Error::MalformedCheckpoint); + }; + let state_sha = digest(handler_state); + (checkpoint, handler_state.to_owned(), state_sha) } - (None, None) => Checkpoint::default(), + (None, None) => (Checkpoint::default(), vec![], "".into()), _ => return Err(Error::MissingCheckpoint), }; Ok(Self { game_addr: game_account.addr.clone(), game_id: 0, - access_version: game_account.access_version, + access_version: checkpoint.access_version, settle_version: game_account.settle_version, status: GameStatus::Idle, nodes, @@ -286,24 +291,21 @@ impl GameContext { timestamp: 0, random_states: vec![], decision_states: vec![], - handler_state: "".into(), + handler_state, checkpoint, sub_games: vec![], init_data: game_account.data.clone(), max_players: game_account.max_players, players, entry_type: game_account.entry_type.clone(), - state_sha: "".into(), + state_sha, }) } pub fn init_account(&self) -> Result { - let checkpoint = self.checkpoint.get_data(self.game_id); - Ok(InitAccount { max_players: self.max_players, data: self.init_data.clone(), - checkpoint, }) } @@ -513,16 +515,16 @@ impl GameContext { &mut self, random_id: RandomId, player_addr: String, - indexes: Vec, + indices: Vec, ) -> Result<()> { let rnd_st = self.get_random_state_mut(random_id)?; - rnd_st.assign(player_addr, indexes)?; + rnd_st.assign(player_addr, indices)?; Ok(()) } - pub fn reveal(&mut self, random_id: RandomId, indexes: Vec) -> Result<()> { + pub fn reveal(&mut self, random_id: RandomId, indices: Vec) -> Result<()> { let rnd_st = self.get_random_state_mut(random_id)?; - rnd_st.reveal(indexes)?; + rnd_st.reveal(indices)?; Ok(()) } @@ -827,16 +829,16 @@ impl GameContext { for Assign { random_id, - indexes, + indices, player_id, } in assigns.into_iter() { let addr = self.id_to_addr(player_id)?; - self.assign(random_id, addr, indexes)?; + self.assign(random_id, addr, indices)?; } - for Reveal { random_id, indexes } in reveals.into_iter() { - self.reveal(random_id, indexes)?; + for Reveal { random_id, indices } in reveals.into_iter() { + self.reveal(random_id, indices)?; } for Release { decision_id } in releases.into_iter() { @@ -855,16 +857,7 @@ impl GameContext { if let Some(state) = handler_state { self.set_handler_state_raw(state.clone()); - if is_init && self.checkpoint.is_empty() { - self.checkpoint = Checkpoint::new( - self.game_id, - self.access_version, - self.settle_version, - state.clone(), - ); - } - - if is_checkpoint { + if is_checkpoint || is_init { // Clear the random states self.random_states.clear(); self.decision_states.clear(); @@ -883,7 +876,7 @@ impl GameContext { return Ok(EventEffects { settles, transfers, - checkpoint: is_checkpoint.then(|| self.checkpoint.clone()), + checkpoint: (is_checkpoint || is_init).then(|| self.checkpoint.clone()), launch_sub_games, bridge_events, start_game, @@ -908,16 +901,6 @@ impl GameContext { } } - pub fn set_versions(&mut self, access_version: u64, settle_version: u64) -> Result<()> { - if self.settle_version != settle_version { - return Err(Error::InvalidCheckpoint); - } - - self.access_version = access_version; - - Ok(()) - } - pub fn init_data(&self) -> Vec { self.init_data.clone() } diff --git a/core/src/random.rs b/core/src/random.rs index 8f5cf550..260b5170 100644 --- a/core/src/random.rs +++ b/core/src/random.rs @@ -334,7 +334,7 @@ impl RandomState { } } - pub fn assign(&mut self, addr: S, indexes: Vec) -> Result<()> + pub fn assign(&mut self, addr: S, indices: Vec) -> Result<()> where S: ToOwned, { @@ -345,7 +345,7 @@ impl RandomState { return Err(Error::InvalidRandomStatus(self.status.clone())); } - if indexes + if indices .iter() .filter_map(|i| self.get_ciphertext(*i)) .any(|c| matches!(c.owner, CipherOwner::Assigned(_) | CipherOwner::Revealed)) @@ -353,7 +353,7 @@ impl RandomState { return Err(Error::CiphertextAlreadyAssigned); } - for i in indexes.into_iter() { + for i in indices.into_iter() { if let Some(c) = self.get_ciphertext_mut(i) { c.owner = CipherOwner::Assigned(addr.to_owned()); } @@ -386,7 +386,7 @@ impl RandomState { self.secret_shares.push(share); } } - pub fn reveal(&mut self, indexes: Vec) -> Result<()> { + pub fn reveal(&mut self, indices: Vec) -> Result<()> { if !matches!( self.status, RandomStatus::Shared | RandomStatus::Ready | RandomStatus::WaitingSecrets @@ -394,7 +394,7 @@ impl RandomState { return Err(Error::InvalidRandomStatus(self.status.clone())); } - for i in indexes.into_iter() { + for i in indices.into_iter() { if let Some(c) = self.get_ciphertext_mut(i) { if c.owner != CipherOwner::Revealed { c.owner = CipherOwner::Revealed; diff --git a/core/src/types/accounts/game_account.rs b/core/src/types/accounts/game_account.rs index fd706eda..ed979162 100644 --- a/core/src/types/accounts/game_account.rs +++ b/core/src/types/accounts/game_account.rs @@ -2,7 +2,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use race_api::{prelude::InitAccount, types::EntryLock}; -use crate::checkpoint::{Checkpoint, CheckpointOnChain}; +use crate::checkpoint::CheckpointOnChain; #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -219,27 +219,17 @@ pub struct GameAccount { } impl GameAccount { - pub fn derive_init_account(&self, checkpoint: &Checkpoint) -> InitAccount { + pub fn derive_init_account(&self) -> InitAccount { InitAccount { max_players: self.max_players, data: self.data.clone(), - checkpoint: checkpoint.get_data(0), } } - pub fn derive_init_account_with_empty_checkpoint(&self) -> InitAccount { + pub fn derive_checkpoint_init_account(&self) -> InitAccount { InitAccount { max_players: self.max_players, data: self.data.clone(), - checkpoint: None, - } - } - - pub fn derive_checkpoint_init_account(&self, checkpoint: &Checkpoint) -> InitAccount { - InitAccount { - max_players: self.max_players, - data: self.data.clone(), - checkpoint: checkpoint.get_data(0), } } } diff --git a/examples/draw-card/src/lib.rs b/examples/draw-card/src/lib.rs index 2d2e4b93..675d768d 100644 --- a/examples/draw-card/src/lib.rs +++ b/examples/draw-card/src/lib.rs @@ -8,7 +8,8 @@ //! the same hands). If player B folds, player A wins the pot. //! Players switch positions in each round. -use arrayref::{array_mut_ref, mut_array_refs}; +use std::collections::BTreeMap; + use race_api::prelude::*; use race_proc_macro::game_handler; @@ -55,7 +56,8 @@ pub struct Player { pub struct DrawCard { pub last_winner: Option, pub random_id: RandomId, - pub players: Vec, + pub player_map: BTreeMap, + pub action_order: Vec, // player ids in action order pub stage: GameStage, pub pot: u64, pub bet: u64, @@ -65,26 +67,6 @@ pub struct DrawCard { } impl DrawCard { - fn set_winner(&mut self, effect: &mut Effect, winner_index: usize) -> Result<(), HandleError> { - let players = array_mut_ref![self.players, 0, 2]; - let (player_0, player_1) = mut_array_refs![players, 1, 1]; - let player_0 = &mut player_0[0]; - let player_1 = &mut player_1[0]; - - if winner_index == 0 { - effect.settle(Settle::add(player_0.id, self.pot - player_0.bet))?; - effect.settle(Settle::sub(player_1.id, player_1.bet))?; - player_0.balance += self.pot; - } else { - effect.settle(Settle::add(player_1.id, self.pot - player_1.bet))?; - effect.settle(Settle::sub(player_0.id, player_0.bet))?; - player_1.balance += self.pot; - } - - effect.checkpoint(); - effect.wait_timeout(NEXT_GAME_TIMEOUT); - Ok(()) - } fn custom_handle_event( &mut self, @@ -96,9 +78,9 @@ impl DrawCard { GameEvent::Bet(amount) => { if self.stage == GameStage::Betting { let player = self - .players - .get_mut(0) - .ok_or(HandleError::Custom("Player not found".into()))?; + .player_map + .get_mut(&self.action_order[0]) + .ok_or(HandleError::InvalidPlayer)?; if sender != player.id { return Err(HandleError::InvalidPlayer); } @@ -118,9 +100,9 @@ impl DrawCard { GameEvent::Call => { if self.stage == GameStage::Reacting { let player = self - .players - .get_mut(1) - .ok_or(HandleError::Custom("Player not found".into()))?; + .player_map + .get_mut(&self.action_order[1]) + .ok_or(HandleError::InvalidPlayer)?; if sender.ne(&player.id) { return Err(HandleError::InvalidPlayer); } @@ -169,25 +151,16 @@ fn is_better_than(card_a: &str, card_b: &str) -> bool { impl GameHandler for DrawCard { - fn init_state(_effect: &mut Effect, init_account: InitAccount) -> Result { + fn init_state(init_account: InitAccount) -> Result { let AccountData { blind_bet, min_bet, max_bet, } = init_account.data()?; - let players: Vec = init_account - .players - .into_iter() - .map(|p| Player { - id: p.id, - balance: p.balance, - bet: 0, - }) - .collect(); Ok(Self { last_winner: None, random_id: 0, - players, + players: vec![], bet: 0, pot: 0, stage: GameStage::Dealing, @@ -245,8 +218,8 @@ impl GameHandler for DrawCard { Event::Join { players } => { for p in players.into_iter() { self.players.push(Player { - id: p.id, - balance: p.balance, + id: p.id(), + balance: 0, bet: 0, }); } @@ -287,8 +260,9 @@ impl GameHandler for DrawCard { Event::Leave { player_id } => { if let Some(player_idx) = self.players.iter().position(|p| p.id.eq(&player_id)) { - self.set_winner(effect, if player_idx == 0 { 1 } else { 0 })?; - effect.settle(Settle::eject(player_id))?; + let player = self.players.remove(player_idx); + effect.settle(player.id, player.balance)?; + effect.wait_timeout(NEXT_GAME_TIMEOUT); effect.checkpoint(); } else { return Err(HandleError::InvalidPlayer); diff --git a/examples/minimal/src/lib.rs b/examples/minimal/src/lib.rs index 6175555b..818449a9 100644 --- a/examples/minimal/src/lib.rs +++ b/examples/minimal/src/lib.rs @@ -30,10 +30,7 @@ impl Minimal { impl GameHandler for Minimal { - fn init_state(_effect: &mut Effect, init_account: InitAccount) -> Result { - if let Some(checkpoint) = init_account.checkpoint()? { - return Ok(checkpoint); - } + fn init_state(init_account: InitAccount) -> Result { let account_data: MinimalAccountData = init_account.data()?; Ok(Self { n: account_data.init_n, diff --git a/examples/raffle/src/lib.rs b/examples/raffle/src/lib.rs index 01c0e2c3..1e493d21 100644 --- a/examples/raffle/src/lib.rs +++ b/examples/raffle/src/lib.rs @@ -13,14 +13,12 @@ const DRAW_TIMEOUT: u64 = 30_000; #[derive(BorshSerialize, BorshDeserialize)] struct Player { pub id: u64, - pub balance: u64, } impl From for Player { fn from(value: GamePlayer) -> Self { Self { - id: value.id, - balance: value.balance, + id: value.id(), } } } @@ -32,6 +30,7 @@ struct Raffle { players: Vec, random_id: RandomId, draw_time: u64, + prize_pool: u64, } impl Raffle { @@ -46,13 +45,13 @@ impl GameHandler for Raffle { /// Initialize handler state with on-chain game account data. fn init_state(_effect: &mut Effect, init_account: InitAccount) -> HandleResult { - let players = init_account.players.into_iter().map(Into::into).collect(); let draw_time = 0; Ok(Self { last_winner: None, - players, + players: vec![], random_id: 0, draw_time, + prize_pool: 0, }) } @@ -77,6 +76,12 @@ impl GameHandler for Raffle { } } + Event::Deposit { deposits } => { + for d in deposits { + self.prize_pool += d.balance(); + } + } + // Reveal the first idess when randomness is ready. Event::RandomnessReady { .. } => { effect.reveal(self.random_id, vec![0]); @@ -104,10 +109,9 @@ impl GameHandler for Raffle { for p in self.players.iter() { if p.id != winner { - effect.settle(Settle::add(winner, p.balance))?; - effect.settle(Settle::sub(p.id, p.balance))?; + effect.settle(p.id, 0)?; } - effect.settle(Settle::eject(p.id))?; + effect.settle(p.id, self.prize_pool)?; } effect.checkpoint(); self.last_winner = Some(winner); diff --git a/facade/src/main.rs b/facade/src/main.rs index ead7580c..956b2cfb 100644 --- a/facade/src/main.rs +++ b/facade/src/main.rs @@ -333,7 +333,9 @@ async fn register_server(params: Params<'_>, context: Arc>) -> Rp endpoint, }; let context = context.lock().await; - context.add_server(&server)?; + if context.get_server_account(&server_addr)?.is_none() { + context.add_server(&server)?; + } Ok(()) } @@ -405,10 +407,12 @@ async fn get_profile( ) -> RpcResult>> { let addr: String = params.one()?; let context = context.lock().await; - match context.get_player_info(&addr)? { + let ret = match context.get_player_info(&addr)? { Some(player_info) => Ok(Some(borsh::to_vec(&player_info.profile).unwrap())), None => Ok(None), - } + }; + println!("? Player profile: {:?}", ret); + ret } async fn vote(params: Params<'_>, context: Arc>) -> RpcResult<()> { diff --git a/js/sdk-core/src/accounts.ts b/js/sdk-core/src/accounts.ts index 6c8b6304..852889a7 100644 --- a/js/sdk-core/src/accounts.ts +++ b/js/sdk-core/src/accounts.ts @@ -1,52 +1,46 @@ -import { field, array, struct, option, enums, variant } from '@race-foundation/borsh' import { CheckpointOnChain } from './checkpoint' +import { IKind, UnionFromValues } from './types' -export enum EntryLock { - Open = 0, - JoinOnly = 1, - DepositOnly = 2, - Closed = 3, -} +export const ENTRY_LOCKS = ['Open', 'JoinOnly', 'DepositOnly', 'Closed'] as const +export type EntryLock = UnionFromValues -export interface IPlayerJoin { +export interface PlayerJoin { readonly addr: string readonly position: number readonly accessVersion: bigint readonly verifyKey: string } -export interface IPlayerDeposit { +export interface PlayerDeposit { readonly addr: string readonly amount: bigint readonly settleVersion: bigint } -export interface IServerJoin { +export interface ServerJoin { readonly addr: string readonly endpoint: string readonly accessVersion: bigint readonly verifyKey: string } -export enum VoteType { - ServerVoteTransactorDropOff = 0, - ClientVoteTransactorDropOff = 1, -} +export const VOTE_TYPES = ['ServerVoteTransactorDropOff', 'ClientVoteTransactorDropOff'] as const +export type VoteType = UnionFromValues -export interface IVote { +export interface Vote { readonly voter: string readonly votee: string readonly voteType: VoteType } -export interface IGameRegistration { +export interface GameRegistration { readonly title: string readonly addr: string readonly regTime: bigint readonly bundleAddr: string } -export interface IGameAccount { +export interface GameAccount { readonly addr: string readonly title: string readonly bundleAddr: string @@ -69,25 +63,25 @@ export interface IGameAccount { readonly entryLock: EntryLock } -export interface IServerAccount { +export interface ServerAccount { readonly addr: string readonly endpoint: string } -export interface IGameBundle { +export interface GameBundle { readonly addr: string readonly uri: string readonly name: string readonly data: Uint8Array } -export interface IPlayerProfile { +export interface PlayerProfile { readonly addr: string readonly nick: string readonly pfp: string | undefined } -export interface IRegistrationAccount { +export interface RegistrationAccount { readonly addr: string readonly isPrivate: boolean readonly size: number @@ -95,7 +89,7 @@ export interface IRegistrationAccount { readonly games: GameRegistration[] } -export interface IToken { +export interface Token { readonly addr: string readonly icon: string readonly name: string @@ -103,28 +97,7 @@ export interface IToken { readonly decimals: number } -export class Token implements IToken { - readonly addr!: string - readonly icon!: string - readonly name!: string - readonly symbol!: string - readonly decimals!: number - constructor(fields: IToken) { - Object.assign(this, fields) - } -} - -export interface ITokenWithBalance extends IToken { - readonly addr: string - readonly icon: string - readonly name: string - readonly symbol: string - readonly decimals: number - readonly amount: bigint - readonly uiAmount: string -} - -export class TokenWithBalance implements ITokenWithBalance { +export class TokenWithBalance { readonly addr!: string readonly icon!: string readonly name!: string @@ -132,14 +105,14 @@ export class TokenWithBalance implements ITokenWithBalance { readonly decimals!: number readonly amount!: bigint readonly uiAmount!: string - constructor(token: IToken, amount: bigint) { + constructor(token: Token, amount: bigint) { Object.assign(this, token) this.amount = amount this.uiAmount = (Number(amount) / Math.pow(10, token.decimals)).toLocaleString() } } -export interface INft { +export interface Nft { readonly addr: string readonly image: string readonly name: string @@ -148,354 +121,75 @@ export interface INft { readonly metadata: any } -export interface IRecipientAccount { +export interface RecipientAccount { readonly addr: string readonly capAddr: string | undefined - readonly slots: IRecipientSlot[] + readonly slots: RecipientSlot[] } -const RECIPIENT_SLOT_TYPE = { - Nft: 0, - Token: 1, -} as const +export const RECIPIENT_SLOT_TYPES = [ 'Nft', 'Token' ] as const -type RecipientSlotType = (typeof RECIPIENT_SLOT_TYPE)[keyof typeof RECIPIENT_SLOT_TYPE] +export type RecipientSlotType = UnionFromValues -export interface IRecipientSlot { +export interface RecipientSlot { readonly id: number readonly slotType: RecipientSlotType readonly tokenAddr: string - readonly shares: IRecipientSlotShare[] + readonly shares: RecipientSlotShare[] readonly balance: bigint } -export interface IRecipientSlotShare { +export interface RecipientSlotShare { readonly owner: RecipientSlotOwner readonly weights: number readonly claimAmount: bigint } -export abstract class RecipientSlotOwner {} - -@variant(0) -export class RecipientSlotOwnerUnassigned extends RecipientSlotOwner { - @field('string') - identifier!: string - constructor(fields: any) { - super() - Object.assign(this, fields) - } -} - -@variant(1) -export class RecipientSlotOwnerAssigned extends RecipientSlotOwner { - @field('string') - addr!: string - constructor(fields: any) { - super() - Object.assign(this, fields) - } -} - -export type EntryTypeKind = 'Invalid' | 'Cash' | 'Ticket' | 'Gating' | 'Disabled' - -export interface IEntryTypeKind { - kind(): EntryTypeKind -} - -export abstract class EntryType implements IEntryTypeKind { - kind(): EntryTypeKind { - return 'Invalid' - } -} - -@variant(0) -export class EntryTypeCash extends EntryType implements IEntryTypeKind { - @field('u64') - minDeposit!: bigint - @field('u64') - maxDeposit!: bigint - constructor(fields: any) { - super() - Object.assign(this, fields) - Object.setPrototypeOf(this, EntryTypeCash.prototype) - } - kind(): EntryTypeKind { - return 'Cash' - } -} - -@variant(1) -export class EntryTypeTicket extends EntryType implements IEntryTypeKind { - @field('u64') - amount!: bigint - constructor(fields: any) { - super() - Object.assign(this, fields) - Object.setPrototypeOf(this, EntryTypeTicket.prototype) - } - kind(): EntryTypeKind { - return 'Ticket' - } -} +export type RecipientSlotOwnerKind = IKind -@variant(2) -export class EntryTypeGating extends EntryType implements IEntryTypeKind { - @field('string') - collection!: string - constructor(fields: any) { - super() - Object.assign(this, fields) - Object.setPrototypeOf(this, EntryTypeGating.prototype) - } - kind(): EntryTypeKind { - return 'Gating' - } -} +export type RecipientSlotOwnerUnassigned = { + readonly identifier: string +} & RecipientSlotOwnerKind<'unassigned'> -@variant(3) -export class EntryTypeDisabled extends EntryType implements IEntryTypeKind { - constructor(_: any) { - super() - Object.setPrototypeOf(this, EntryTypeDisabled.prototype) - } - kind(): EntryTypeKind { - return 'Disabled' - } -} - -export class Nft implements INft { - @field('string') - readonly addr!: string - @field('string') - readonly image!: string - @field('string') - readonly name!: string - @field('string') - readonly symbol!: string - @field(option('string')) - readonly collection: string | undefined - readonly metadata: any - constructor(fields: INft) { - Object.assign(this, fields) - } -} - -export class ServerAccount implements IServerAccount { - @field('string') - readonly addr!: string - @field('string') - readonly endpoint!: string - constructor(fields: IServerAccount) { - Object.assign(this, fields) - } -} +export type RecipientSlotOwnerAssigned = { + readonly addr: string +} & RecipientSlotOwnerKind<'assigned'> -export class PlayerJoin implements IPlayerJoin { - @field('string') - readonly addr!: string - @field('u16') - readonly position!: number - @field('u64') - readonly accessVersion!: bigint - @field('string') - readonly verifyKey!: string - constructor(fields: IPlayerJoin) { - Object.assign(this, fields) - } -} +export type RecipientSlotOwner = + | RecipientSlotOwnerUnassigned + | RecipientSlotOwnerAssigned -export class ServerJoin implements IServerJoin { - @field('string') - readonly addr!: string - @field('string') - readonly endpoint!: string - @field('u64') - readonly accessVersion!: bigint - @field('string') - readonly verifyKey!: string - constructor(fields: IServerJoin) { - Object.assign(this, fields) - } -} +export type EntryTypeKind = IKind -export class PlayerDeposit implements IPlayerDeposit { - @field('string') - readonly addr!: string - @field('u64') - readonly amount!: bigint - @field('u64') - readonly settleVersion!: bigint - constructor(fields: IPlayerDeposit) { - Object.assign(this, fields) - } -} +export type EntryTypeCash = { + readonly minDeposit: bigint + readonly maxDeposit: bigint +} & EntryTypeKind<'cash'> -export class Vote implements IVote { - @field('string') - readonly voter!: string - @field('string') - readonly votee!: string - @field('u8') - readonly voteType!: VoteType - constructor(fields: IVote) { - Object.assign(this, fields) - } -} +export type EntryTypeTicket = { + readonly amount: bigint +} & EntryTypeKind<'ticket'> -export class GameAccount implements IGameAccount { - @field('string') - readonly addr!: string - @field('string') - readonly title!: string - @field('string') - readonly bundleAddr!: string - @field('string') - readonly tokenAddr!: string - @field('string') - readonly ownerAddr!: string - @field('u64') - readonly settleVersion!: bigint - @field('u64') - readonly accessVersion!: bigint - @field(array(struct(PlayerJoin))) - readonly players!: PlayerJoin[] - @field(array(struct(PlayerDeposit))) - readonly deposits!: PlayerDeposit[] - @field(array(struct(ServerJoin))) - readonly servers!: ServerJoin[] - @field(option('string')) - readonly transactorAddr: string | undefined - @field(array(struct(Vote))) - readonly votes!: Vote[] - @field(option('u64')) - readonly unlockTime: bigint | undefined - @field('u16') - readonly maxPlayers!: number - @field('u32') - readonly dataLen!: number - @field('u8-array') - readonly data!: Uint8Array - @field(enums(EntryType)) - readonly entryType!: EntryType - @field('string') - readonly recipientAddr!: string - @field(option(struct(CheckpointOnChain))) - readonly checkpointOnChain: CheckpointOnChain | undefined - @field('u8') - readonly entryLock!: EntryLock - constructor(fields: IGameAccount) { - Object.assign(this, fields) - } -} +export type EntryTypeGating = { + readonly collection: string +} & EntryTypeKind<'gating'> -export class GameBundle implements IGameBundle { - @field('string') - readonly addr!: string - @field('string') - readonly uri!: string - @field('string') - readonly name!: string - @field('u8-array') - readonly data!: Uint8Array +export type EntryTypeDisabled = { +} & EntryTypeKind<'disabled'> - constructor(fields: IGameBundle) { - Object.assign(this, fields) - } -} - -export class GameRegistration implements IGameRegistration { - @field('string') - readonly title!: string - @field('string') - readonly addr!: string - @field('u64') - readonly regTime!: bigint - @field('string') - readonly bundleAddr!: string - constructor(fields: IGameRegistration) { - Object.assign(this, fields) - } -} - -export class RegistrationAccount implements IRegistrationAccount { - @field('string') - readonly addr!: string - @field('bool') - readonly isPrivate!: boolean - @field('u16') - readonly size!: number - @field(option('string')) - readonly owner!: string | undefined - @field(array(struct(GameRegistration))) - readonly games!: GameRegistration[] - constructor(fields: IRegistrationAccount) { - Object.assign(this, fields) - } -} +export type EntryType = + | EntryTypeCash + | EntryTypeTicket + | EntryTypeGating + | EntryTypeDisabled /** * The registration account data with games consolidated. */ -export class RegistrationWithGames { - readonly addr!: string - readonly isPrivate!: boolean - readonly size!: number +export interface RegistrationWithGames { + readonly addr: string + readonly isPrivate: boolean + readonly size: number readonly owner: string | undefined - readonly games!: GameAccount[] - constructor(fields: Object) { - Object.assign(this, fields) - } -} - -export class PlayerProfile implements IPlayerProfile { - @field('string') - readonly addr!: string - @field('string') - readonly nick!: string - @field(option('string')) - readonly pfp: string | undefined - constructor(fields: IPlayerProfile) { - Object.assign(this, fields) - } -} - -export class RecipientSlotShare implements IRecipientSlotShare { - @field(enums(RecipientSlotOwner)) - owner!: RecipientSlotOwner - @field('u16') - weights!: number - @field('u64') - claimAmount!: bigint - constructor(fields: IRecipientSlotShare) { - Object.assign(this, fields) - } -} - -export class RecipientSlot implements IRecipientSlot { - @field('u8') - id!: number - @field('u8') - slotType!: RecipientSlotType - @field('string') - tokenAddr!: string - @field(array(struct(RecipientSlotShare))) - shares!: IRecipientSlotShare[] - @field('u64') - balance!: bigint - constructor(fields: IRecipientSlot) { - Object.assign(this, fields) - } -} - -export class RecipientAccount implements IRecipientAccount { - @field('string') - addr!: string - @field(option('string')) - capAddr: string | undefined - @field(array(struct(RecipientSlot))) - slots!: IRecipientSlot[] - constructor(fields: IRecipientAccount) { - Object.assign(this, fields) - } + readonly games: GameAccount[] } diff --git a/js/sdk-core/src/app-client.ts b/js/sdk-core/src/app-client.ts index 8cda2361..35ce77ce 100644 --- a/js/sdk-core/src/app-client.ts +++ b/js/sdk-core/src/app-client.ts @@ -3,14 +3,14 @@ import { GameContext } from './game-context' import { ITransport, JoinError, JoinResponse } from './transport' import { IWallet } from './wallet' import { Handler } from './handler' -import { Encryptor, IEncryptor } from './encryptor' +import { Encryptor, IEncryptor, sha256String } from './encryptor' import { SdkError } from './error' import { Client } from './client' import { IStorage, getTtlCache, makeBundleCacheKey, 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 { GameAccount, GameBundle, Token } from './accounts' import { ConnectionStateCallbackFunction, EventCallbackFunction, @@ -129,7 +129,7 @@ export class AppClient extends BaseClient { const gameBundle = await getGameBundle(transport, storage, bundleCacheKey, gameAccount.bundleAddr) const transactorAddr = gameAccount.transactorAddr - if (transactorAddr === undefined) { + if (transactorAddr === undefined || gameAccount.checkpointOnChain === undefined) { throw SdkError.gameNotServed(gameAddr) } console.info(`Transactor address: ${transactorAddr}`) @@ -162,13 +162,18 @@ export class AppClient extends BaseClient { if (checkpointOffChain !== undefined && gameAccount.checkpointOnChain !== undefined) { checkpoint = Checkpoint.fromParts(checkpointOffChain, gameAccount.checkpointOnChain) } else { - checkpoint = Checkpoint.default() + throw new Error('Game not served') } + const handlerState = checkpoint.getData(0) + if (handlerState === undefined) { + throw new Error('Malformed checkpoint') + } + const stateSha = await sha256String(handlerState) - const gameContext = new GameContext(gameAccount, checkpoint) + const gameContext = new GameContext(gameAccount, checkpoint, handlerState, stateSha) console.info('Game Context:', clone(gameContext)) - let token: IToken | undefined = await transport.getToken(gameAccount.tokenAddr) + let token: Token | undefined = await transport.getToken(gameAccount.tokenAddr) if (token === undefined) { const decimals = await transport.getTokenDecimals(gameAccount.tokenAddr) if (decimals === undefined) { @@ -404,7 +409,7 @@ export async function getGameBundle( return gameBundle } -export function makeGameInfo(gameAccount: GameAccount, token: IToken): GameInfo { +export function makeGameInfo(gameAccount: GameAccount, token: Token): GameInfo { const info: GameInfo = { gameAddr: gameAccount.addr, title: gameAccount.title, @@ -417,10 +422,5 @@ export function makeGameInfo(gameAccount: GameAccount, token: IToken): GameInfo token, } - if (gameAccount.entryType instanceof EntryTypeCash) { - info.minDeposit = gameAccount.entryType.minDeposit - info.maxDeposit = gameAccount.entryType.maxDeposit - } - return info } diff --git a/js/sdk-core/src/app-helper.ts b/js/sdk-core/src/app-helper.ts index 54ff877a..c03eb02b 100644 --- a/js/sdk-core/src/app-helper.ts +++ b/js/sdk-core/src/app-helper.ts @@ -1,4 +1,4 @@ -import { EntryTypeCash, GameAccount, INft, IToken, ITokenWithBalance, RecipientAccount } from './accounts' +import { GameAccount, Nft, Token, TokenWithBalance, RecipientAccount } from './accounts' import { GAME_ACCOUNT_CACHE_TTL, NFT_CACHE_TTL, TOKEN_CACHE_TTL } from './common' import { ResponseHandle, ResponseStream } from './response' import { @@ -85,7 +85,7 @@ export class AppHelper { throw new Error('Invalid title') } - if (params.entryType instanceof EntryTypeCash) { + if (params.entryType.kind === 'cash') { const entryType = params.entryType if (entryType.minDeposit <= 0) { throw new Error('Invalid minDeposit') @@ -93,6 +93,11 @@ export class AppHelper { if (entryType.maxDeposit < entryType.minDeposit) { throw new Error('Invalid maxDeposit') } + } else if (params.entryType.kind === 'ticket') { + const entryType = params.entryType + if (entryType.amount <= 0) { + throw new Error('Invalid ticket price') + } } else { throw new Error('Unsupported entry type') } @@ -229,15 +234,15 @@ export class AppHelper { * * @return A list of token info. */ - async listTokens(tokenAddrs: string[]): Promise { + async listTokens(tokenAddrs: string[]): Promise { if (this.#storage === undefined) { return await this.#transport.listTokens(tokenAddrs) } else { - let res: IToken[] = [] + let res: Token[] = [] let queryAddrs: string[] = [] for (const addr of tokenAddrs) { const cacheKey = makeTokenCacheKey(this.#transport.chain, addr) - const token = getTtlCache(this.#storage, cacheKey) + const token = getTtlCache(this.#storage, cacheKey) if (token !== undefined) { console.debug('Get token info from cache: %s', addr) res.push(token) @@ -260,7 +265,7 @@ export class AppHelper { * * @return A list of token info. */ - async listTokensWithBalance(walletAddr: string, tokenAddrs: string[]): Promise { + async listTokensWithBalance(walletAddr: string, tokenAddrs: string[]): Promise { return await this.#transport.listTokensWithBalance(walletAddr, tokenAddrs) } @@ -272,7 +277,7 @@ export class AppHelper { * * @return A list of nfts. */ - async listNfts(walletAddr: string, collection: string | undefined = undefined): Promise { + async listNfts(walletAddr: string, collection: string | undefined = undefined): Promise { const nfts = await this.#transport.listNfts(walletAddr) if (collection === undefined) { return nfts @@ -286,12 +291,12 @@ export class AppHelper { * * @param addr - The address of NFT */ - async getNft(addr: string): Promise { + async getNft(addr: string): Promise { if (this.#storage === undefined) { return await this.#transport.getNft(addr) } else { const cacheKey = makeNftCacheKey(this.#transport.chain, addr) - const cached = getTtlCache(this.#storage, cacheKey) + const cached = getTtlCache(this.#storage, cacheKey) if (cached !== undefined) { return cached } else { @@ -346,7 +351,8 @@ export class AppHelper { for (const share of slot.shares) { totalClaimed += share.claimAmount totalWeights += share.weights - if (share.owner === wallet.walletAddr) { + + if (share.owner.kind === 'assigned' && share.owner.addr === wallet.walletAddr) { weights += share.weights claimed += share.claimAmount } diff --git a/js/sdk-core/src/base-client.ts b/js/sdk-core/src/base-client.ts index 643476e2..fa80af2f 100644 --- a/js/sdk-core/src/base-client.ts +++ b/js/sdk-core/src/base-client.ts @@ -344,6 +344,7 @@ export class BaseClient { const { txState } = frame if (txState instanceof PlayerConfirming) { txState.confirmPlayers.forEach(p => { + console.info('Load profile for:', p.addr) this.__onLoadProfile(p.id, p.addr) }) } @@ -364,6 +365,7 @@ export class BaseClient { } for (const node of frame.newPlayers) { this.__gameContext.addNode(node.addr, node.accessVersion, 'player') + console.info('Load profile for:', node.addr) this.__onLoadProfile(node.accessVersion, node.addr) } this.__gameContext.setAccessVersion(frame.accessVersion) @@ -376,7 +378,6 @@ export class BaseClient { } else if (frame instanceof BroadcastFrameEventHistories) { console.group(`${this.__logPrefix}Receive event histories`, frame) try { - await this.__handler.initState(this.__gameContext) await this.__checkStateSha(frame.stateSha, 'checkpoint-state-sha-mismatch') this.__invokeEventCallback(new Init()) diff --git a/js/sdk-core/src/broadcast-frames.ts b/js/sdk-core/src/broadcast-frames.ts index 3dede324..c76a8314 100644 --- a/js/sdk-core/src/broadcast-frames.ts +++ b/js/sdk-core/src/broadcast-frames.ts @@ -1,9 +1,37 @@ import { TxState } from './tx-state' -import { PlayerJoin, ServerJoin } from './accounts' import { array, enums, field, option, struct, variant } from '@race-foundation/borsh' import { EventHistory, GameEvent } from './events' import { CheckpointOffChain } from './checkpoint' import { Message } from './message' +import { Fields } from './types' + +export class BroadcastPlayerJoin { + @field('string') + readonly addr!: string + @field('u16') + readonly position!: number + @field('u64') + readonly accessVersion!: bigint + @field('string') + readonly verifyKey!: string + constructor(fields: Fields) { + Object.assign(this, fields) + } +} + +export class BroadcastServerJoin { + @field('string') + readonly addr!: string + @field('string') + readonly endpoint!: string + @field('u64') + readonly accessVersion!: bigint + @field('string') + readonly verifyKey!: string + constructor(fields: Fields) { + Object.assign(this, fields) + } +} export type BroadcastFrameKind = 'Invalid' | 'Event' | 'Message' | 'TxState' | 'Sync' | 'EventHistories' @@ -65,10 +93,10 @@ export class BroadcastFrameTxState extends BroadcastFrame { @variant(3) export class BroadcastFrameSync extends BroadcastFrame { - @field(array(struct(PlayerJoin))) - newPlayers!: PlayerJoin[] - @field(array(struct(ServerJoin))) - newServers!: ServerJoin[] + @field(array(struct(BroadcastPlayerJoin))) + newPlayers!: BroadcastPlayerJoin[] + @field(array(struct(BroadcastServerJoin))) + newServers!: BroadcastServerJoin[] @field('string') transactor_addr!: string @field('u64') diff --git a/js/sdk-core/src/client.ts b/js/sdk-core/src/client.ts index 49cb5f99..14d34c0c 100644 --- a/js/sdk-core/src/client.ts +++ b/js/sdk-core/src/client.ts @@ -2,26 +2,25 @@ import { AttachGameParams, AttachResponse, IConnection, SubmitEventParams } from import { IEncryptor } from './encryptor' import { SecretState } from './secret-state' import { GameContext } from './game-context' -import { Id } from './types' type OpIdent = | { kind: 'random-secret' - randomId: Id + randomId: number toAddr: string | undefined index: number } | { kind: 'answer-secret' - decisionId: Id + decisionId: number } | { kind: 'lock' - randomId: Id + randomId: number } | { kind: 'mask' - randomId: Id + randomId: number } export class Client { @@ -80,7 +79,7 @@ export class Client { this.__opHist.splice(0) } - async decrypt(ctx: GameContext, randomId: Id): Promise> { + async decrypt(ctx: GameContext, randomId: number): Promise> { let randomState = ctx.getRandomState(randomId) let options = randomState.options let revealed = await this.__encryptor.decryptWithSecrets( diff --git a/js/sdk-core/src/decision-state.ts b/js/sdk-core/src/decision-state.ts index 0475eecf..b0bb1360 100644 --- a/js/sdk-core/src/decision-state.ts +++ b/js/sdk-core/src/decision-state.ts @@ -1,4 +1,4 @@ -import { Ciphertext, Digest, Id, Secret } from './types' +import { Ciphertext, Digest, Secret } from './types' export type DecisionStatus = 'asked' | 'answered' | 'releasing' | 'released' @@ -12,13 +12,13 @@ export class Answer { } export class DecisionState { - id: Id + id: number owner: string status: DecisionStatus answer: Answer | undefined secret: Secret | undefined value: string | undefined - constructor(id: Id, owner: string) { + constructor(id: number, owner: string) { this.id = id this.owner = owner this.status = 'asked' diff --git a/js/sdk-core/src/effect.ts b/js/sdk-core/src/effect.ts index 44292702..24430537 100644 --- a/js/sdk-core/src/effect.ts +++ b/js/sdk-core/src/effect.ts @@ -2,7 +2,7 @@ import { RandomSpec } from './random-state' import { HandleError } from './error' import { GameContext } from './game-context' import { enums, field, map, option, struct, array } from '@race-foundation/borsh' -import { Fields, Id } from './types' +import { Fields } from './types' import { InitAccount } from './init-account' import { ContextPlayer } from './game-context' import { EntryLock } from './accounts' @@ -160,11 +160,11 @@ export class Effect { } static fromContext(context: GameContext, isInit: boolean) { - const revealed = new Map>() + const revealed = new Map>() for (const st of context.randomStates) { revealed.set(st.id, st.revealed) } - const answered = new Map() + const answered = new Map() for (const st of context.decisionStates) { answered.set(st.id, st.value!) } diff --git a/js/sdk-core/src/events.ts b/js/sdk-core/src/events.ts index df5f7ad6..599f5c40 100644 --- a/js/sdk-core/src/events.ts +++ b/js/sdk-core/src/events.ts @@ -1,5 +1,5 @@ import { field, array, enums, option, variant, struct } from '@race-foundation/borsh' -import { Fields, Id } from './types' +import { Fields } from './types' export type EventKind = | 'Invalid' // an invalid value @@ -69,7 +69,7 @@ export class Random extends SecretShare { @field(option('string')) toAddr!: string | undefined @field('usize') - randomId!: Id + randomId!: number @field('usize') index!: number @field('u8-array') @@ -86,7 +86,7 @@ export class Answer extends SecretShare { @field('string') fromAddr!: string @field('usize') - decisionId!: Id + decisionId!: number @field('u8-array') secret!: Uint8Array constructor(fields: Fields) { @@ -184,7 +184,7 @@ export class Mask extends GameEvent implements IEventKind { @field('u64') sender!: bigint @field('usize') - randomId!: Id + randomId!: number @field(array('u8-array')) ciphertexts!: Uint8Array[] constructor(fields: Fields) { @@ -212,7 +212,7 @@ export class Lock extends GameEvent implements IEventKind { @field('u64') sender!: bigint @field('usize') - randomId!: Id + randomId!: number @field(array(struct(CiphertextAndDigest))) ciphertextsAndDigests!: CiphertextAndDigest[] constructor(fields: Fields) { @@ -228,7 +228,7 @@ export class Lock extends GameEvent implements IEventKind { @variant(6) export class RandomnessReady extends GameEvent implements IEventKind { @field('usize') - randomId!: Id + randomId!: number constructor(fields: Fields) { super() Object.assign(this, fields) @@ -322,7 +322,7 @@ export class DrawRandomItems extends GameEvent implements IEventKind { @field('u64') sender!: bigint @field('usize') - randomId!: Id + randomId!: number @field(array('usize')) indexes!: number[] constructor(fields: Fields) { @@ -365,7 +365,7 @@ export class AnswerDecision extends GameEvent implements IEventKind { @field('u64') sender!: bigint @field('usize') - decisionId!: Id + decisionId!: number @field('u8-array') ciphertext!: Uint8Array @field('u8-array') diff --git a/js/sdk-core/src/game-context.ts b/js/sdk-core/src/game-context.ts index 32ee79c8..5286da85 100644 --- a/js/sdk-core/src/game-context.ts +++ b/js/sdk-core/src/game-context.ts @@ -16,8 +16,8 @@ import { } from './events' import { InitAccount } from './init-account' import { Effect, EmitBridgeEvent, SubGame, Settle, Transfer } from './effect' -import { EntryType, EntryTypeDisabled, GameAccount } from './accounts' -import { Ciphertext, Digest, Fields, Id } from './types' +import { EntryType, GameAccount } from './accounts' +import { Ciphertext, Digest, Fields } from './types' import { clone } from './utils' import rfdc from 'rfdc' import { sha256String } from './encryptor' @@ -90,7 +90,7 @@ export class GameContext { entryType: EntryType stateSha: string - constructor(gameAccount: GameAccount, checkpoint: Checkpoint) { + constructor(gameAccount: GameAccount, checkpoint: Checkpoint, handlerState: Uint8Array, stateSha: string) { if (checkpoint === undefined) { throw new Error('Missing checkpoint') } @@ -150,14 +150,14 @@ export class GameContext { this.timestamp = 0n this.randomStates = [] this.decisionStates = [] - this.handlerState = Uint8Array.of() + this.handlerState = handlerState this.checkpoint = checkpoint this.subGames = [] this.initData = gameAccount.data this.maxPlayers = gameAccount.maxPlayers this.players = players this.entryType = gameAccount.entryType - this.stateSha = '' + this.stateSha = stateSha } subContext(subGame: SubGame): GameContext { @@ -177,7 +177,7 @@ export class GameContext { c.subGames = [] c.initData = subGame.initAccount.data c.maxPlayers = subGame.initAccount.maxPlayers - c.entryType = new EntryTypeDisabled({}) + c.entryType = { kind: 'disabled' } c.players = [] return c } @@ -187,11 +187,9 @@ export class GameContext { } initAccount(): InitAccount { - const checkpoint = this.checkpoint.getData(this.gameId) return new InitAccount({ maxPlayers: this.maxPlayers, data: this.initData, - checkpoint, }) } @@ -270,7 +268,7 @@ export class GameContext { } } - getRandomState(randomId: Id): RandomState { + getRandomState(randomId: number): RandomState { if (randomId <= 0) { throw new Error('Invalid random id: ' + randomId) } @@ -281,7 +279,7 @@ export class GameContext { return st } - getDecisionState(decisionId: Id): DecisionState { + getDecisionState(decisionId: number): DecisionState { if (decisionId <= 0) { throw new Error('Invalid decision id: ' + decisionId) } @@ -292,17 +290,17 @@ export class GameContext { return st } - assign(randomId: Id, playerAddr: string, indexes: number[]) { + assign(randomId: number, playerAddr: string, indexes: number[]) { const st = this.getRandomState(randomId) st.assign(playerAddr, indexes) } - reveal(randomId: Id, indexes: number[]) { + reveal(randomId: number, indexes: number[]) { const st = this.getRandomState(randomId) st.reveal(indexes) } - isRandomReady(randomId: Id): boolean { + isRandomReady(randomId: number): boolean { const k = this.getRandomState(randomId).status.kind return k === 'ready' || k === 'waiting-secrets' } @@ -343,7 +341,7 @@ export class GameContext { this.accessVersion = accessVersion } - initRandomState(spec: RandomSpec): Id { + initRandomState(spec: RandomSpec): number { const randomId = this.randomStates.length + 1 const owners = this.nodes.filter(n => n.status.kind === 'ready' && n.mode !== 'player').map(n => n.addr) const randomState = new RandomState(randomId, spec, owners) @@ -363,19 +361,19 @@ export class GameContext { } } - randomizeAndMask(addr: string, randomId: Id, ciphertexts: Ciphertext[]) { + randomizeAndMask(addr: string, randomId: number, ciphertexts: Ciphertext[]) { let st = this.getRandomState(randomId) st.mask(addr, ciphertexts) this.dispatchRandomizationTimeout(randomId) } - lock(addr: string, randomId: Id, ciphertextsAndTests: CiphertextAndDigest[]) { + lock(addr: string, randomId: number, ciphertextsAndTests: CiphertextAndDigest[]) { let st = this.getRandomState(randomId) st.lock(addr, ciphertextsAndTests) this.dispatchRandomizationTimeout(randomId) } - dispatchRandomizationTimeout(randomId: Id) { + dispatchRandomizationTimeout(randomId: number) { const noDispatch = this.dispatch === undefined let st = this.getRandomState(randomId) const statusKind = st.status.kind @@ -399,29 +397,29 @@ export class GameContext { this.settleVersion += 1n } - addRevealedRandom(randomId: Id, revealed: Map) { + addRevealedRandom(randomId: number, revealed: Map) { const st = this.getRandomState(randomId) st.addRevealed(revealed) } - addRevealedAnswer(decisionId: Id, revealed: string) { + addRevealedAnswer(decisionId: number, revealed: string) { const st = this.getDecisionState(decisionId) st.addReleased(revealed) } - ask(owner: string): Id { + ask(owner: string): number { const id = this.decisionStates.length + 1 const st = new DecisionState(id, owner) this.decisionStates.push(st) return id } - answerDecision(id: Id, owner: string, ciphertext: Ciphertext, digest: Digest) { + answerDecision(id: number, owner: string, ciphertext: Ciphertext, digest: Digest) { const st = this.getDecisionState(id) st.setAnswer(owner, ciphertext, digest) } - getRevealed(randomId: Id): Map { + getRevealed(randomId: number): Map { let st = this.getRandomState(randomId) return st.revealed } diff --git a/js/sdk-core/src/init-account.ts b/js/sdk-core/src/init-account.ts index 2fcb56d2..661e3396 100644 --- a/js/sdk-core/src/init-account.ts +++ b/js/sdk-core/src/init-account.ts @@ -10,18 +10,16 @@ export class InitAccount { readonly maxPlayers: number @field('u8-array') readonly data: Uint8Array - @field(option('u8-array')) - readonly checkpoint: Uint8Array | undefined constructor(fields: Fields) { this.maxPlayers = fields.maxPlayers this.data = fields.data - this.checkpoint = fields.checkpoint } serialize(): Uint8Array { return serialize(InitAccount) } + static deserialize(data: Uint8Array) { return deserialize(InitAccount, data) } diff --git a/js/sdk-core/src/profile-loader.ts b/js/sdk-core/src/profile-loader.ts index 4c32013c..a596ef7e 100644 --- a/js/sdk-core/src/profile-loader.ts +++ b/js/sdk-core/src/profile-loader.ts @@ -1,4 +1,4 @@ -import { INft } from './accounts' +import { Nft } from './accounts' import { NFT_CACHE_TTL } from './common' import { getTtlCache, IStorage, makeNftCacheKey, setTtlCache } from './storage' import { ITransport } from './transport' @@ -28,11 +28,12 @@ export class ProfileLoader { async __loadProfile(playerAddr: string): Promise { const profile = await this.transport.getPlayerProfile(playerAddr) if (profile === undefined) { + console.warn(`Player profile missing ${playerAddr}`) return undefined } else { let p if (profile.pfp !== undefined) { - let pfp: INft | undefined + let pfp: Nft | undefined if (this.storage === undefined) { pfp = await this.transport.getNft(profile.pfp) } else { @@ -44,9 +45,6 @@ export class ProfileLoader { setTtlCache(this.storage, cacheKey, pfp, NFT_CACHE_TTL) } } - if (pfp === undefined) { - return undefined - } } p = { pfp, addr: profile.addr, nick: profile.nick } } else { diff --git a/js/sdk-core/src/random-state.ts b/js/sdk-core/src/random-state.ts index 5eb66587..c383ff65 100644 --- a/js/sdk-core/src/random-state.ts +++ b/js/sdk-core/src/random-state.ts @@ -1,11 +1,11 @@ import { field, map, variant, array } from '@race-foundation/borsh' -import { Ciphertext, Digest, Fields, Id, Secret } from './types' +import { Ciphertext, Digest, Fields, Secret } from './types' import { CiphertextAndDigest } from './events' export interface SecretIdent { fromAddr: string toAddr: string | undefined - randomId: Id + randomId: number index: number } @@ -125,7 +125,7 @@ export type RandomStatus = } export class RandomState { - id: Id + id: number size: number owners: string[] options: string[] @@ -135,7 +135,7 @@ export class RandomState { secretShares: Share[] revealed: Map - constructor(id: Id, spec: RandomSpec, owners: string[]) { + constructor(id: number, spec: RandomSpec, owners: string[]) { if (owners.length === 0) { throw new Error('No enough servers') } diff --git a/js/sdk-core/src/secret-state.ts b/js/sdk-core/src/secret-state.ts index 81ad2cdd..5e6f365b 100644 --- a/js/sdk-core/src/secret-state.ts +++ b/js/sdk-core/src/secret-state.ts @@ -1,5 +1,4 @@ import { IEncryptor } from './encryptor' -import { Id } from './types' export class SecretState { #encryptor: IEncryptor @@ -9,9 +8,9 @@ export class SecretState { clear() {} - isRandomLoaded(id: Id): boolean { + isRandomLoaded(id: number): boolean { return true } - genRandomStates(id: Id, size: number): any {} + genRandomStates(id: number, size: number): any {} } diff --git a/js/sdk-core/src/storage.ts b/js/sdk-core/src/storage.ts index 6e64c5f8..d1511798 100644 --- a/js/sdk-core/src/storage.ts +++ b/js/sdk-core/src/storage.ts @@ -5,6 +5,21 @@ export interface IStorage { setItem(key: string, value: any): void } +export class TemporaryStorage { + data: Map + constructor() { + this.data = new Map() + } + getItem(key: string): string | null { + const x = this.data.get(key) + if (x === undefined) return null + return x + } + setItem(key: string, value: string) { + this.data.set(key, value) + } +} + export type TtlCache = { expire: number value: T diff --git a/js/sdk-core/src/transport.ts b/js/sdk-core/src/transport.ts index edf9c6b0..18772485 100644 --- a/js/sdk-core/src/transport.ts +++ b/js/sdk-core/src/transport.ts @@ -3,16 +3,15 @@ import { GameAccount, GameBundle, ServerAccount, - PlayerProfile, VoteType, RegistrationAccount, - INft, - IToken, + Nft, + Token, RegistrationWithGames, RecipientAccount, EntryType, - ITokenWithBalance, - IPlayerProfile, + TokenWithBalance, + PlayerProfile, } from './accounts' import { IStorage } from './storage' import { ResponseHandle } from './response' @@ -76,12 +75,17 @@ export type JoinResponse = { } export type DepositParams = { - playerAddr: string gameAddr: string amount: bigint settleVersion: bigint } +export type DepositResponse = { + signature: string +} + +export type DepositError = 'invalid-deposit' | 'game-not-served' | 'game-not-found' + export type VoteParams = { gameAddr: string voteType: VoteType @@ -95,7 +99,7 @@ export type CreatePlayerProfileParams = { } export type CreatePlayerProfileResponse = { - profile: IPlayerProfile + profile: PlayerProfile signature: string } @@ -165,7 +169,7 @@ export interface ITransport { join(wallet: IWallet, params: JoinParams, resp: ResponseHandle): Promise - // deposit(wallet: IWallet, params: DepositParams): Promise> + deposit(wallet: IWallet, params: DepositParams, resp: ResponseHandle): Promise // vote(wallet: IWallet, params: VoteParams): Promise> @@ -209,15 +213,15 @@ export interface ITransport { getTokenDecimals(addr: string): Promise - getToken(addr: string): Promise + getToken(addr: string): Promise - getNft(addr: string): Promise + getNft(addr: string): Promise - listTokens(tokenAddrs: string[]): Promise + listTokens(tokenAddrs: string[]): Promise - listTokensWithBalance(walletAddr: string, tokenAddrs: string[], storage?: IStorage): Promise + listTokensWithBalance(walletAddr: string, tokenAddrs: string[], storage?: IStorage): Promise - listNfts(walletAddr: string): Promise + listNfts(walletAddr: string): Promise recipientClaim( wallet: IWallet, diff --git a/js/sdk-core/src/types.ts b/js/sdk-core/src/types.ts index c95144a5..83fb8016 100644 --- a/js/sdk-core/src/types.ts +++ b/js/sdk-core/src/types.ts @@ -1,24 +1,32 @@ -import { EntryType, INft, IToken } from './accounts' +import { EntryType, Nft, Token } from './accounts' import { Message } from './message' import { ConnectionState } from './connection' import { GameEvent } from './events' import { GameContextSnapshot } from './game-context-snapshot' import { TxState } from './tx-state' -export type Id = number export type Ciphertext = Uint8Array + export type Secret = Uint8Array + export type Digest = Uint8Array + export type Fields = {[K in keyof T as T[K] extends Function ? never: K]: T[K]} +export type Result = { ok: T } | { err: E } + +export type IKind = { kind: T } + +export type Indices = Exclude['length'], T['length']> + +export type UnionFromValues = T extends readonly string[] ? T[number] : never + export type GameInfo = { gameAddr: string title: string maxPlayers: number - minDeposit?: bigint - maxDeposit?: bigint entryType: EntryType - token: IToken + token: Token tokenAddr: string bundleAddr: string data: Uint8Array @@ -26,7 +34,7 @@ export type GameInfo = { } export type PlayerProfileWithPfp = { - pfp: INft | undefined + pfp: Nft | undefined addr: string nick: string } @@ -56,5 +64,3 @@ export type ProfileCallbackFunction = (id: bigint | undefined, profile: PlayerPr export type LoadProfileCallbackFunction = (id: bigint, addr: string) => void export type ErrorCallbackFunction = (error: ErrorKind, arg: any) => void - -export type Result = { ok: T } | { err: E } diff --git a/js/sdk-facade/src/accounts.ts b/js/sdk-facade/src/accounts.ts new file mode 100644 index 00000000..4b9de04f --- /dev/null +++ b/js/sdk-facade/src/accounts.ts @@ -0,0 +1,407 @@ + +import { field, array, struct, option, enums, variant } from '@race-foundation/borsh' +import { CheckpointOnChain, ENTRY_LOCKS, Fields, Indices, RECIPIENT_SLOT_TYPES, VOTE_TYPES } from '@race-foundation/sdk-core' +import * as RaceCore from '@race-foundation/sdk-core' + +type RecipientSlotType = Indices + +export abstract class RecipientSlotOwner { + generalize(): RaceCore.RecipientSlotOwner { + if (this instanceof RecipientSlotOwnerUnassigned) { + return { kind: 'unassigned', identifier: this.identifier } + } else if (this instanceof RecipientSlotOwnerAssigned) { + return { kind: 'assigned', addr: this.addr } + } else { + throw new Error('Invalid RecipientSlotOwner') + } + } +} + +@variant(0) +export class RecipientSlotOwnerUnassigned extends RecipientSlotOwner { + @field('string') + identifier!: string + constructor(fields: any) { + super() + Object.assign(this, fields) + } +} + +@variant(1) +export class RecipientSlotOwnerAssigned extends RecipientSlotOwner { + @field('string') + addr!: string + constructor(fields: any) { + super() + Object.assign(this, fields) + } +} + +export type EntryTypeKind = 'Invalid' | 'Cash' | 'Ticket' | 'Gating' | 'Disabled' + +export interface IEntryTypeKind { + kind(): EntryTypeKind +} + +export abstract class EntryType implements IEntryTypeKind { + kind(): EntryTypeKind { + return 'Invalid' + } + generalize(): RaceCore.EntryType { + if (this instanceof EntryTypeCash) { + return { + kind: 'cash', minDeposit: this.minDeposit, maxDeposit: this.maxDeposit + } + } else if (this instanceof EntryTypeTicket) { + return { + kind: 'ticket', amount: this.amount + } + } else if (this instanceof EntryTypeGating) { + return { + kind: 'gating', collection: this.collection + } + } else { + return { + kind: 'disabled' + } + } + } +} + +@variant(0) +export class EntryTypeCash extends EntryType implements IEntryTypeKind { + @field('u64') + minDeposit!: bigint + @field('u64') + maxDeposit!: bigint + constructor(fields: any) { + super() + Object.assign(this, fields) + Object.setPrototypeOf(this, EntryTypeCash.prototype) + } + kind(): EntryTypeKind { + return 'Cash' + } +} + +@variant(1) +export class EntryTypeTicket extends EntryType implements IEntryTypeKind { + @field('u64') + amount!: bigint + constructor(fields: any) { + super() + Object.assign(this, fields) + Object.setPrototypeOf(this, EntryTypeTicket.prototype) + } + kind(): EntryTypeKind { + return 'Ticket' + } +} + +@variant(2) +export class EntryTypeGating extends EntryType implements IEntryTypeKind { + @field('string') + collection!: string + constructor(fields: any) { + super() + Object.assign(this, fields) + Object.setPrototypeOf(this, EntryTypeGating.prototype) + } + kind(): EntryTypeKind { + return 'Gating' + } +} + +@variant(3) +export class EntryTypeDisabled extends EntryType implements IEntryTypeKind { + constructor(_: any) { + super() + Object.setPrototypeOf(this, EntryTypeDisabled.prototype) + } + kind(): EntryTypeKind { + return 'Disabled' + } +} + +export class Nft { + @field('string') + readonly addr!: string + @field('string') + readonly image!: string + @field('string') + readonly name!: string + @field('string') + readonly symbol!: string + @field(option('string')) + readonly collection: string | undefined + readonly metadata: any + constructor(fields: Fields) { + Object.assign(this, fields) + } +} + +export class ServerAccount { + @field('string') + readonly addr!: string + @field('string') + readonly endpoint!: string + constructor(fields: Fields) { + Object.assign(this, fields) + } +} + +export class PlayerJoin { + @field('string') + readonly addr!: string + @field('u16') + readonly position!: number + @field('u64') + readonly accessVersion!: bigint + @field('string') + readonly verifyKey!: string + constructor(fields: Fields) { + Object.assign(this, fields) + } +} + +export class ServerJoin { + @field('string') + readonly addr!: string + @field('string') + readonly endpoint!: string + @field('u64') + readonly accessVersion!: bigint + @field('string') + readonly verifyKey!: string + constructor(fields: Fields) { + Object.assign(this, fields) + } +} + +export class PlayerDeposit { + @field('string') + readonly addr!: string + @field('u64') + readonly amount!: bigint + @field('u64') + readonly settleVersion!: bigint + constructor(fields: Fields) { + Object.assign(this, fields) + } +} + +export type VoteType = Indices + +export class Vote { + @field('string') + readonly voter!: string + @field('string') + readonly votee!: string + @field('u8') + readonly voteType!: VoteType + constructor(fields: Fields) { + Object.assign(this, fields) + } + generalize(): RaceCore.Vote { + return { + voter: this.voter, + votee: this.votee, + voteType: VOTE_TYPES[this.voteType], + } + } +} + +export type EntryLock = Indices + +export class GameAccount { + @field('string') + readonly addr!: string + @field('string') + readonly title!: string + @field('string') + readonly bundleAddr!: string + @field('string') + readonly tokenAddr!: string + @field('string') + readonly ownerAddr!: string + @field('u64') + readonly settleVersion!: bigint + @field('u64') + readonly accessVersion!: bigint + @field(array(struct(PlayerJoin))) + readonly players!: PlayerJoin[] + @field(array(struct(PlayerDeposit))) + readonly deposits!: PlayerDeposit[] + @field(array(struct(ServerJoin))) + readonly servers!: ServerJoin[] + @field(option('string')) + readonly transactorAddr: string | undefined + @field(array(struct(Vote))) + readonly votes!: Vote[] + @field(option('u64')) + readonly unlockTime: bigint | undefined + @field('u16') + readonly maxPlayers!: number + @field('u32') + readonly dataLen!: number + @field('u8-array') + readonly data!: Uint8Array + @field(enums(EntryType)) + readonly entryType!: EntryType + @field('string') + readonly recipientAddr!: string + @field(option(struct(CheckpointOnChain))) + readonly checkpointOnChain: CheckpointOnChain | undefined + @field('u8') + readonly entryLock!: EntryLock + constructor(fields: Fields) { + Object.assign(this, fields) + } + generalize() { + return { + addr: this.addr, + title: this.title, + bundleAddr: this.bundleAddr, + ownerAddr: this.ownerAddr, + tokenAddr: this.tokenAddr, + transactorAddr: this.transactorAddr, + accessVersion: this.accessVersion, + settleVersion: this.settleVersion, + maxPlayers: this.maxPlayers, + players: this.players, + deposits: this.deposits, + servers: this.servers, + dataLen: this.dataLen, + data: this.data, + votes: this.votes.map(v => v.generalize()), + unlockTime: this.unlockTime, + entryType: this.entryType.generalize(), + recipientAddr: this.recipientAddr, + checkpointOnChain: this.checkpointOnChain, + entryLock: RaceCore.ENTRY_LOCKS[this.entryLock], + } + } +} + +export class GameBundle { + @field('string') + readonly addr!: string + @field('string') + readonly uri!: string + @field('string') + readonly name!: string + @field('u8-array') + readonly data!: Uint8Array + + constructor(fields: Fields) { + Object.assign(this, fields) + } +} + +export class GameRegistration { + @field('string') + readonly title!: string + @field('string') + readonly addr!: string + @field('u64') + readonly regTime!: bigint + @field('string') + readonly bundleAddr!: string + constructor(fields: Fields) { + Object.assign(this, fields) + } +} + +export class RegistrationAccount { + @field('string') + readonly addr!: string + @field('bool') + readonly isPrivate!: boolean + @field('u16') + readonly size!: number + @field(option('string')) + readonly owner!: string | undefined + @field(array(struct(GameRegistration))) + readonly games!: GameRegistration[] + constructor(fields: Fields) { + Object.assign(this, fields) + } +} + +export class PlayerProfile { + @field('string') + readonly addr!: string + @field('string') + readonly nick!: string + @field(option('string')) + readonly pfp: string | undefined + constructor(fields: Fields) { + Object.assign(this, fields) + } + generalize(): RaceCore.PlayerProfile { + return this + } +} + +export class RecipientSlotShare { + @field(enums(RecipientSlotOwner)) + owner!: RecipientSlotOwner + @field('u16') + weights!: number + @field('u64') + claimAmount!: bigint + constructor(fields: Fields) { + Object.assign(this, fields) + } + generalize(): RaceCore.RecipientSlotShare { + return { + owner: this.owner.generalize(), + weights: this.weights, + claimAmount: this.claimAmount + } + } +} + +export class RecipientSlot { + @field('u8') + id!: number + @field('u8') + slotType!: RecipientSlotType + @field('string') + tokenAddr!: string + @field(array(struct(RecipientSlotShare))) + shares!: RecipientSlotShare[] + @field('u64') + balance!: bigint + constructor(fields: Fields) { + Object.assign(this, fields) + } + generalize(): RaceCore.RecipientSlot { + return { + id: this.id, + slotType: RECIPIENT_SLOT_TYPES[this.slotType], + tokenAddr: this.tokenAddr, + shares: this.shares.map(s => s.generalize()), + balance: this.balance, + } + } +} + +export class RecipientAccount { + @field('string') + addr!: string + @field(option('string')) + capAddr: string | undefined + @field(array(struct(RecipientSlot))) + slots!: RecipientSlot[] + constructor(fields: Fields) { + Object.assign(this, fields) + } + generalize(): RaceCore.RecipientAccount { + return { + addr: this.addr, + capAddr: this.capAddr, + slots: this.slots.map(s => s.generalize()) + } + } +} diff --git a/js/sdk-facade/src/facade-transport.ts b/js/sdk-facade/src/facade-transport.ts index d40e0814..0218c0ac 100644 --- a/js/sdk-facade/src/facade-transport.ts +++ b/js/sdk-facade/src/facade-transport.ts @@ -1,29 +1,14 @@ import { makeid } from './utils' import { - CloseGameAccountParams, - CreateGameAccountParams, - CreatePlayerProfileParams, - CreateRegistrationParams, - DepositParams, GameAccount, GameBundle, - INft, - IToken, - ITransport, - IWallet, - JoinParams, + Nft, PlayerProfile, - PublishGameParams, - RecipientAccount, - RecipientClaimParams, - RegisterGameParams, RegistrationAccount, - RegistrationWithGames, ServerAccount, - UnregisterGameParams, - VoteParams, - ITokenWithBalance, - ResponseHandle, +} from './accounts' +import * as RaceCore from '@race-foundation/sdk-core' +import { ResponseHandle, CreateGameError, CreateGameResponse, CreatePlayerProfileError, @@ -33,6 +18,22 @@ import { CreateRecipientParams, CreateRecipientResponse, CreateRecipientError, + DepositResponse, + DepositError, + UnregisterGameParams, + VoteParams, RecipientClaimParams, + RegisterGameParams, + PublishGameParams, + ITransport, + IWallet, + JoinParams, + CloseGameAccountParams, + CreateGameAccountParams, + CreatePlayerProfileParams, + CreateRegistrationParams, + DepositParams, + Token, + TokenWithBalance, } from '@race-foundation/sdk-core' import { deserialize } from '@race-foundation/borsh' import { Chain } from '@race-foundation/sdk-core/lib/types/common' @@ -45,6 +46,12 @@ interface JoinInstruction { accessVersion: bigint } +interface DepositInstruction { + playerAddr: string + gameAddr: string + settleVersion: bigint +} + interface CreatePlayerProfileInstruction { playerAddr: string nick: string @@ -66,7 +73,7 @@ interface CreateGameAccountInstruction { data: number[] } -const nftMap: Record = { +const nftMap: Record = { nft01: { addr: 'nft01', image: 'https://arweave.net/plLA2nFm_TyHDA76v9GAkaH-nUnymuA4cIvRj64BTLs', @@ -85,7 +92,7 @@ const nftMap: Record = { }, } -const tokenMap: Record = { +const tokenMap: Record = { FACADE_NATIVE: { name: 'Native Token', symbol: 'NATIVE', @@ -138,9 +145,21 @@ export class FacadeTransport implements ITransport { closeGameAccount(_wallet: IWallet, _params: CloseGameAccountParams): Promise { throw new Error('Method not implemented.') } - deposit(_wallet: IWallet, _params: DepositParams): Promise { - throw new Error('Method not implemented.') + + async deposit(wallet: IWallet, params: DepositParams, response: ResponseHandle): Promise { + const playerAddr = wallet.walletAddr + const gameAccount = await this.getGameAccount(params.gameAddr) + if (gameAccount === undefined) { + return response.failed('game-not-found') + } + if (params.settleVersion !== gameAccount.settleVersion) { + return response.failed('invalid-deposit') + } + const ix: DepositInstruction = { playerAddr, ...params } + const signature = await this.sendInstruction('deposit', ix) + response.succeed({ signature }) } + vote(_wallet: IWallet, _params: VoteParams): Promise { throw new Error('Method not implemented.') } @@ -160,17 +179,17 @@ export class FacadeTransport implements ITransport { throw new Error('Method not implemented.') } - async listTokens(tokenAddrs: string[]): Promise { + async listTokens(tokenAddrs: string[]): Promise { return Object.values(tokenMap).filter(t => tokenAddrs.includes(t.addr)) } async listTokensWithBalance( walletAddr: string, tokenAddrs: string[], - ): Promise { + ): Promise { const balances = await this.fetchBalances(walletAddr, tokenAddrs) const tokens = Object.values(tokenMap).filter(t => tokenAddrs.includes(t.addr)) - let ret: ITokenWithBalance[] = [] + let ret: TokenWithBalance[] = [] for (const token of tokens) { const amount = balances.get(token.addr) || 0n const uiAmount = (Number(amount) / Math.pow(10, token.decimals)).toString() @@ -208,10 +227,10 @@ export class FacadeTransport implements ITransport { const signature = await this.sendInstruction('join', ix) response.succeed({ signature }) } - async getGameAccount(addr: string): Promise { + async getGameAccount(addr: string): Promise { const data: Uint8Array | undefined = await this.fetchState('get_account_info', [addr]) if (data === undefined) return undefined - return deserialize(GameAccount, data) + return deserialize(GameAccount, data).generalize() } async getGameBundle(addr: string): Promise { const data: Uint8Array | undefined = await this.fetchState('get_game_bundle', [addr]) @@ -234,29 +253,35 @@ export class FacadeTransport implements ITransport { return deserialize(RegistrationAccount, data) } - async getRecipient(_addr: string): Promise { + async getRecipient(_addr: string): Promise { return undefined } - async getRegistrationWithGames(addr: string): Promise { + async getRegistrationWithGames(addr: string): Promise { const data: Uint8Array | undefined = await this.fetchState('get_registration_info', [addr]) if (data === undefined) return undefined const regAccount = deserialize(RegistrationAccount, data) const promises = regAccount.games.map(async g => { return await this.getGameAccount(g.addr) }) - const games = await Promise.all(promises) - return new RegistrationWithGames({ + let games: RaceCore.GameAccount[] = [] + let fetchedGames = await Promise.all(promises) + for (let g of fetchedGames) { + if (g !== undefined) { + games.push(g) + } + } + return { ...regAccount, games, - }) + } } async getTokenDecimals(addr: string): Promise { return tokenMap[addr]?.decimals } - async getToken(addr: string): Promise { + async getToken(addr: string): Promise { return tokenMap[addr] } @@ -273,11 +298,11 @@ export class FacadeTransport implements ITransport { return ret } - async getNft(addr: string): Promise { + async getNft(addr: string): Promise { return nftMap[addr] } - async listNfts(_walletAddr: string): Promise { + async listNfts(_walletAddr: string): Promise { return Object.values(nftMap) } diff --git a/js/sdk-solana/src/accounts.ts b/js/sdk-solana/src/accounts.ts index 247c96a6..327afeb1 100644 --- a/js/sdk-solana/src/accounts.ts +++ b/js/sdk-solana/src/accounts.ts @@ -2,7 +2,6 @@ import { PublicKey } from '@solana/web3.js' import * as _ from 'borsh' import { publicKeyExt } from './utils' import * as RaceCore from '@race-foundation/sdk-core' -import { VoteType, EntryType, EntryLock } from '@race-foundation/sdk-core' import { deserialize, serialize, field, option, array, struct, enums, variant } from '@race-foundation/borsh' export interface IPlayerState { @@ -71,7 +70,7 @@ export interface IGameState { data: Uint8Array votes: IVote[] unlockTime: bigint | undefined - entryType: EntryType + entryType: AEntryType recipientAddr: PublicKey checkpoint: Uint8Array entryLock: EntryLock @@ -90,12 +89,7 @@ export interface IRecipientState { slots: IRecipientSlot[] } -const RECIPIENT_SLOT_TYPE = { - Nft: 0, - Token: 1, -} as const - -type RecipientSlotType = (typeof RECIPIENT_SLOT_TYPE)[keyof typeof RECIPIENT_SLOT_TYPE] +type RecipientSlotType = RaceCore.Indices export interface IRecipientSlot { readonly id: number @@ -132,14 +126,16 @@ export class PlayerState implements IPlayerState { } generalize(addr: PublicKey): RaceCore.PlayerProfile { - return new RaceCore.PlayerProfile({ + return { addr: addr.toBase58(), nick: this.nick, pfp: this.pfpKey?.toBase58(), - }) + } } } +type VoteType = RaceCore.Indices + export class Vote implements IVote { @field(publicKeyExt) voterKey!: PublicKey @@ -151,11 +147,11 @@ export class Vote implements IVote { Object.assign(this, fields) } generalize(): RaceCore.Vote { - return new RaceCore.Vote({ + return { voter: this.voterKey.toBase58(), votee: this.voteeKey.toBase58(), - voteType: this.voteType, - }) + voteType: RaceCore.VOTE_TYPES[this.voteType], + } } } @@ -172,12 +168,12 @@ export class ServerJoin implements IServerJoin { Object.assign(this, fields) } generalize(): RaceCore.ServerJoin { - return new RaceCore.ServerJoin({ + return { addr: this.key.toBase58(), endpoint: this.endpoint, accessVersion: this.accessVersion, verifyKey: this.verifyKey, - }) + } } } @@ -195,12 +191,12 @@ export class PlayerJoin implements IPlayerJoin { Object.assign(this, fields) } generalize(): RaceCore.PlayerJoin { - return new RaceCore.PlayerJoin({ + return { addr: this.key.toBase58(), position: this.position, accessVersion: this.accessVersion, verifyKey: this.verifyKey, - }) + } } } @@ -215,11 +211,90 @@ export class PlayerDeposit implements IPlayerDeposit { Object.assign(this, fields) } generalize(): RaceCore.PlayerDeposit { - return new RaceCore.PlayerDeposit({ + return { addr: this.key.toBase58(), amount: this.amount, settleVersion: this.settleVersion, - }) + } + } +} + +type EntryLock = RaceCore.Indices + +export abstract class AEntryType { + static from(entryType: RaceCore.EntryType) { + if (entryType.kind === 'cash') { + return new EntryTypeCash(entryType) + } else if (entryType.kind === 'ticket') { + return new EntryTypeTicket(entryType) + } else if (entryType.kind === 'gating') { + return new EntryTypeGating(entryType) + } else { + return new EntryTypeDisabled(entryType) + } + } + + generalize(): RaceCore.EntryType { + if (this instanceof EntryTypeCash) { + return { + kind: 'cash', minDeposit: this.minDeposit, maxDeposit: this.maxDeposit + } + } else if (this instanceof EntryTypeTicket) { + return { + kind: 'ticket', amount: this.amount + } + } else if (this instanceof EntryTypeGating) { + return { + kind: 'gating', collection: this.collection + } + } else { + return { + kind: 'disabled' + } + } + } +} + +@variant(0) +export class EntryTypeCash extends AEntryType { + @field('u64') + minDeposit!: bigint + @field('u64') + maxDeposit!: bigint + constructor(fields: RaceCore.Fields) { + super() + Object.assign(this, fields) + Object.setPrototypeOf(this, EntryTypeCash.prototype) + } +} + +@variant(1) +export class EntryTypeTicket extends AEntryType { + @field('u64') + amount!: bigint + constructor(fields: RaceCore.Fields) { + super() + Object.assign(this, fields) + Object.setPrototypeOf(this, EntryTypeTicket.prototype) + } +} + +@variant(2) +export class EntryTypeGating extends AEntryType { + @field('string') + collection!: string + constructor(fields: RaceCore.Fields) { + super() + Object.assign(this, fields) + Object.setPrototypeOf(this, EntryTypeGating.prototype) + } +} + +@variant(3) +export class EntryTypeDisabled extends AEntryType { + constructor(_: RaceCore.Fields) { + super() + Object.setPrototypeOf(this, EntryTypeDisabled.prototype) } } @@ -260,14 +335,14 @@ export class GameState implements IGameState { votes!: Vote[] @field(option('u64')) unlockTime: bigint | undefined - @field(enums(EntryType)) - entryType!: EntryType + @field(enums(AEntryType)) + entryType!: AEntryType @field(publicKeyExt) recipientAddr!: PublicKey @field('u8-array') checkpoint!: Uint8Array @field('u8') - entryLock!: 0 | 1 | 2 | 3 + entryLock!: EntryLock constructor(fields: IGameState) { Object.assign(this, fields) @@ -287,7 +362,7 @@ export class GameState implements IGameState { checkpointOnChain = RaceCore.CheckpointOnChain.fromRaw(this.checkpoint) } - return new RaceCore.GameAccount({ + return { addr: addr.toBase58(), title: this.title, bundleAddr: this.bundleKey.toBase58(), @@ -304,11 +379,11 @@ export class GameState implements IGameState { data: this.data, votes: this.votes.map(v => v.generalize()), unlockTime: this.unlockTime, - entryType: this.entryType, + entryType: this.entryType.generalize(), recipientAddr: this.recipientAddr.toBase58(), checkpointOnChain, - entryLock: this.entryLock - }) + entryLock: RaceCore.ENTRY_LOCKS[this.entryLock], + } } } @@ -325,12 +400,12 @@ export class GameReg implements IGameReg { Object.assign(this, fields) } generalize(): RaceCore.GameRegistration { - return new RaceCore.GameRegistration({ + return { title: this.title, addr: this.gameKey.toBase58(), bundleAddr: this.bundleKey.toBase58(), regTime: this.regTime, - }) + } } } @@ -358,13 +433,13 @@ export class RegistryState implements IRegistryState { } generalize(addr: PublicKey): RaceCore.RegistrationAccount { - return new RaceCore.RegistrationAccount({ + return { addr: addr.toBase58(), isPrivate: this.isPrivate, size: this.size, owner: this.ownerKey.toBase58(), games: this.games.map(g => g.generalize()), - }) + } } } @@ -391,10 +466,10 @@ export class ServerState implements IServerState { } generalize(): RaceCore.ServerAccount { - return new RaceCore.ServerAccount({ + return { addr: this.ownerKey.toBase58(), endpoint: this.endpoint, - }) + } } } @@ -432,19 +507,25 @@ export class RecipientSlotShare implements IRecipientSlotShare { } generalize(): RaceCore.RecipientSlotShare { - let owner: RecipientSlotOwner + let owner: RaceCore.RecipientSlotOwner if (this.owner instanceof RecipientSlotOwnerAssigned) { - owner = new RaceCore.RecipientSlotOwnerAssigned({ addr: this.owner.addr.toBase58() }) + owner = { + kind: 'assigned', + addr: this.owner.addr.toBase58() + } } else if (this.owner instanceof RecipientSlotOwnerUnassigned) { - owner = new RaceCore.RecipientSlotOwnerUnassigned({ identifier: this.owner.identifier }) + owner = { + kind: 'unassigned', + identifier: this.owner.identifier + } } else { throw new Error('Invalid slot owner') } - return new RaceCore.RecipientSlotShare({ + return { owner, weights: this.weights, claimAmount: this.claimAmount, - }) + } } } @@ -464,13 +545,13 @@ export class RecipientSlot implements IRecipientSlot { } generalize(balance: bigint): RaceCore.RecipientSlot { - return new RaceCore.RecipientSlot({ + return { id: this.id, - slotType: this.slotType, + slotType: RaceCore.RECIPIENT_SLOT_TYPES[this.slotType], tokenAddr: this.tokenAddr.toBase58(), shares: this.shares.map(s => s.generalize()), balance, - }) + } } } @@ -495,10 +576,10 @@ export class RecipientState implements IRecipientState { } generalize(addr: string, slots: RaceCore.RecipientSlot[]): RaceCore.RecipientAccount { - return new RaceCore.RecipientAccount({ + return { addr, capAddr: this.capAddr?.toBase58(), slots, - }) + } } } diff --git a/js/sdk-solana/src/instruction.ts b/js/sdk-solana/src/instruction.ts index 1fd2427d..c286c3fb 100644 --- a/js/sdk-solana/src/instruction.ts +++ b/js/sdk-solana/src/instruction.ts @@ -1,14 +1,16 @@ import { PublicKey, SYSVAR_RENT_PUBKEY, SystemProgram, TransactionInstruction } from '@solana/web3.js' import { TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync } from '@solana/spl-token' import { publicKeyExt } from './utils' -import { PROGRAM_ID, METAPLEX_PROGRAM_ID, PLAYER_PROFILE_SEED } from './constants' -import { array, enums, extend, ExtendOptions, field, IExtendWriter, serialize, struct } from '@race-foundation/borsh' +import { PROGRAM_ID, METAPLEX_PROGRAM_ID} from './constants' +import { array, enums, field, serialize, struct } from '@race-foundation/borsh' import { Buffer } from 'buffer' -import { EntryType, RecipientSlotInit } from '@race-foundation/sdk-core' -import { RecipientSlot, RecipientSlotOwner, RecipientSlotOwnerAssigned, RecipientState } from './accounts' +import { EntryType, Fields } from '@race-foundation/sdk-core' +import { AEntryType, RecipientSlotOwner, RecipientSlotOwnerAssigned, RecipientState } from './accounts' // Instruction types +type IxParams = Omit, 'instruction'> + export enum Instruction { CreateGameAccount = 0, CloseGameAccount = 1, @@ -64,12 +66,12 @@ export class CreateGameAccountData extends Serialize { title: string = '' @field('u16') maxPlayers: number = 0 - @field(enums(EntryType)) - entryType!: EntryType + @field(enums(AEntryType)) + entryType!: AEntryType @field('u8-array') data: Uint8Array = Uint8Array.from([]) - constructor(params: Partial) { + constructor(params: IxParams) { super() Object.assign(this, params) } @@ -87,12 +89,12 @@ export class JoinGameData extends Serialize { @field('string') verifyKey: string - constructor(amount: bigint, accessVersion: bigint, position: number, verifyKey: string) { + constructor(params: IxParams) { super() - this.amount = amount - this.accessVersion = accessVersion - this.position = position - this.verifyKey = verifyKey + this.amount = params.amount + this.accessVersion = params.accessVersion + this.position = params.position + this.verifyKey = params.verifyKey } } @@ -106,11 +108,11 @@ export class PublishGameData extends Serialize { @field('string') symbol: string - constructor(uri: string, name: string, symbol: string) { + constructor(params: IxParams) { super() - this.uri = uri - this.name = name - this.symbol = symbol + this.uri = params.uri + this.name = params.name + this.symbol = params.symbol } } @@ -142,7 +144,7 @@ export class SlotInit { @field(array(struct(SlotShareInit))) initShares!: SlotShareInit[] - constructor(fields: any) { + constructor(fields: Fields) { Object.assign(this, fields) } } @@ -154,9 +156,9 @@ export class CreateRecipientData extends Serialize { @field(array(struct(SlotInit))) slots: SlotInit[] - constructor(slots: SlotInit[]) { + constructor(params: IxParams) { super() - this.slots = slots + this.slots = params.slots } } @@ -245,7 +247,7 @@ export function registerGame(opts: RegisterGameOptions): TransactionInstruction export function createGameAccount(opts: CreateGameOptions): TransactionInstruction { const params = new CreateGameAccountData({ title: opts.title, - entryType: opts.entryType, + entryType: AEntryType.from(opts.entryType), maxPlayers: opts.maxPlayers, data: opts.data, }) @@ -376,7 +378,7 @@ export function join(opts: JoinOptions): TransactionInstruction { } = opts let [pda, _] = PublicKey.findProgramAddressSync([gameAccountKey.toBuffer()], PROGRAM_ID) - const data = new JoinGameData(amount, accessVersion, position, verifyKey).serialize() + const data = new JoinGameData({amount, accessVersion, position, verifyKey}).serialize() return new TransactionInstruction({ keys: [ @@ -454,7 +456,7 @@ export function publishGame(opts: PublishGameOptions): TransactionInstruction { ) let ata = getAssociatedTokenAddressSync(mint, ownerKey) - let data = new PublishGameData(uri, name, symbol).serialize() + let data = new PublishGameData({ uri, name, symbol }).serialize() return new TransactionInstruction({ keys: [ @@ -522,18 +524,18 @@ export function createRecipient(opts: CreateRecipientOpts): TransactionInstructi let keys = [ { pubkey: payerKey, - isSigner: true, - isWritable: false, + isSigner: true, + isWritable: false, }, { - pubkey: capKey, - isSigner: false, - isWritable: false, + pubkey: capKey, + isSigner: false, + isWritable: false, }, { - pubkey: recipientKey, - isSigner: false, - isWritable: false, + pubkey: recipientKey, + isSigner: false, + isWritable: false, }, { pubkey: TOKEN_PROGRAM_ID, @@ -544,7 +546,7 @@ export function createRecipient(opts: CreateRecipientOpts): TransactionInstructi slots.forEach(slot => keys.push({ pubkey: slot.stakeAddr, isSigner: false, isWritable: false })) - const data = new CreateRecipientData(slots).serialize() + const data = new CreateRecipientData({ slots }).serialize() return new TransactionInstruction({ keys, programId: PROGRAM_ID, data diff --git a/js/sdk-solana/src/solana-transport.ts b/js/sdk-solana/src/solana-transport.ts index 8ebc32ea..ae7f915e 100644 --- a/js/sdk-solana/src/solana-transport.ts +++ b/js/sdk-solana/src/solana-transport.ts @@ -40,16 +40,13 @@ import { PlayerProfile, ServerAccount, RegistrationAccount, - IToken, - INft, + Token, + Nft, RegistrationWithGames, RecipientAccount, RecipientSlot, RecipientClaimParams, - EntryTypeCash, - ITokenWithBalance, TokenWithBalance, - Token, ResponseHandle, CreateGameResponse, CreateGameError, @@ -64,13 +61,14 @@ import { CreateRecipientResponse, CreateRecipientError, CreateRecipientParams, - EntryTypeTicket, + DepositResponse, + DepositError, } from '@race-foundation/sdk-core' import * as instruction from './instruction' import { GAME_ACCOUNT_LEN, NAME_LEN, PROFILE_ACCOUNT_LEN, PLAYER_PROFILE_SEED, SERVER_PROFILE_SEED, RECIPIENT_ACCOUNT_LEN } from './constants' -import { GameState, PlayerState, RecipientSlotOwnerAssigned, RecipientSlotOwnerUnassigned, RecipientState, RegistryState, ServerState } from './accounts' +import { EntryTypeCash, EntryTypeTicket, GameState, PlayerState, RecipientSlotOwnerAssigned, RecipientSlotOwnerUnassigned, RecipientState, RegistryState, ServerState } from './accounts' import { join } from './instruction' import { PROGRAM_ID, METAPLEX_PROGRAM_ID } from './constants' @@ -328,7 +326,7 @@ export class SolanaTransport implements ITransport { } } - async deposit(_wallet: IWallet, _params: DepositParams, _response: ResponseHandle): Promise { + async deposit(wallet: IWallet, params: DepositParams, response: ResponseHandle): Promise { throw new Error('unimplemented') } @@ -608,7 +606,7 @@ export class SolanaTransport implements ITransport { let files: any[] = json['properties']['files'] let wasm_file = files.find(f => f['type'] == 'application/wasm') - return new GameBundle({ addr, uri: wasm_file['uri'], name: trimString(name), data: new Uint8Array(0) }) + return { addr, uri: wasm_file['uri'], name: trimString(name), data: new Uint8Array(0) } } async getPlayerProfile(addr: string): Promise { @@ -652,19 +650,17 @@ export class SolanaTransport implements ITransport { if (regAccount === undefined) return undefined const keys = regAccount.games.map(g => new PublicKey(g.addr)) const gameStates = await this._getMultiGameStates(keys) - let games: Array = [] + let games: Array = [] for (let i = 0; i < gameStates.length; i++) { const gs = gameStates[i] - if (gs === undefined) { - games.push(undefined) - } else { + if (gs !== undefined) { games.push(gs.generalize(keys[i])) } } - return new RegistrationWithGames({ + return { ...regAccount, games, - }) + } } async getRecipient(addr: string): Promise { @@ -708,7 +704,7 @@ export class SolanaTransport implements ITransport { return mint.decimals } - async getToken(addr: string): Promise { + async getToken(addr: string): Promise { const mintKey = new PublicKey(addr) try { const mint = await getMint(this.#conn, mintKey, 'finalized') @@ -778,7 +774,7 @@ export class SolanaTransport implements ITransport { return [name, symbol, icon] } - async listTokens(tokenAddrs: string[]): Promise { + async listTokens(tokenAddrs: string[]): Promise { if (tokenAddrs.length > 30) { throw new Error('Too many token addresses in a row') } @@ -817,7 +813,7 @@ export class SolanaTransport implements ITransport { } if (decimals !== undefined && name !== undefined && symbol !== undefined && icon !== undefined) { - const token = new Token({ addr, name, symbol, icon, decimals }) + const token = { addr, name, symbol, icon, decimals } console.debug('Found token:', token) results.push(token) } @@ -832,7 +828,7 @@ export class SolanaTransport implements ITransport { async listTokensWithBalance( walletAddr: string, tokenAddrs: string[], - ): Promise { + ): Promise { if (tokenAddrs.length > 30) { throw new Error('Too many token addresses in a row') } @@ -889,7 +885,7 @@ export class SolanaTransport implements ITransport { return results } - async getNft(addr: string | PublicKey): Promise { + async getNft(addr: string | PublicKey): Promise { let mintKey: PublicKey if (addr instanceof PublicKey) { @@ -938,7 +934,7 @@ export class SolanaTransport implements ITransport { } } - async listNfts(walletAddr: string): Promise { + async listNfts(walletAddr: string): Promise { let nfts = [] const ownerKey = new PublicKey(walletAddr) const parsedTokenAccounts = await this.#conn.getParsedTokenAccountsByOwner(ownerKey, { diff --git a/proc-macro/src/lib.rs b/proc-macro/src/lib.rs index b9c33911..020d8d66 100644 --- a/proc-macro/src/lib.rs +++ b/proc-macro/src/lib.rs @@ -17,11 +17,11 @@ use syn::{parse_macro_input, ItemStruct}; /// /// impl GameHandler for S { /// -/// fn init_state(context: &mut Effect, init_account: InitAccount) -> HandleResult { +/// fn init_state(init_account: InitAccount) -> HandleResult { /// Ok(Self {}) /// } /// -/// fn handle_event(&mut self, context: &mut Effect, event: Event) -> HandleResult<()> { +/// fn handle_event(&mut self, effect: &mut Effect, event: Event) -> HandleResult<()> { /// Ok(()) /// } /// } @@ -91,7 +91,7 @@ pub fn game_handler(_metadata: TokenStream, input: TokenStream) -> TokenStream { } else { return 2 }; - match #s_idt::init_state(&mut effect, init_account) { + match #s_idt::init_state(init_account) { Ok(handler) => effect.__set_handler_state(&handler), Err(e) => effect.__set_error(e), } diff --git a/test/src/handler_helpers.rs b/test/src/handler_helpers.rs index 9ca3a395..67b82dbf 100644 --- a/test/src/handler_helpers.rs +++ b/test/src/handler_helpers.rs @@ -30,8 +30,8 @@ impl TestHandler { pub fn init_state(context: &mut GameContext) -> Result<(Self, EventEffects)> { let mut new_context = context.clone(); let init_account = new_context.init_account()?; - let mut effect = new_context.derive_effect(true); - let handler = H::init_state(&mut effect, init_account)?; + let effect = new_context.derive_effect(true); + let handler = H::init_state(init_account)?; let event_effects = new_context.apply_effect(effect)?; patch_handle_event_effects(&mut new_context, &event_effects); swap(context, &mut new_context); diff --git a/test/src/transport_helpers.rs b/test/src/transport_helpers.rs index 637181b9..e32b400c 100644 --- a/test/src/transport_helpers.rs +++ b/test/src/transport_helpers.rs @@ -231,17 +231,17 @@ mod tests { transport.simulate_states(states); let addr = test_game_addr(); - assert_eq!(Some(ga_0), transport.get_game_account(&addr, QueryMode::Finalized).await?); - assert_eq!(Some(ga_1), transport.get_game_account(&addr, QueryMode::Finalized).await?); - assert_eq!(Some(ga_2), transport.get_game_account(&addr, QueryMode::Finalized).await?); - assert_eq!(None, transport.get_game_account(&addr, QueryMode::Finalized).await?); + assert_eq!(Some(ga_0), transport.get_game_account(&addr).await?); + assert_eq!(Some(ga_1), transport.get_game_account(&addr).await?); + assert_eq!(Some(ga_2), transport.get_game_account(&addr).await?); + assert_eq!(None, transport.get_game_account(&addr).await?); Ok(()) } #[tokio::test] async fn test_settle() { let transport = DummyTransport::default(); - let settles = vec![SettleWithAddr::add("Alice", 100), SettleWithAddr::add("Bob", 100)]; + let settles = vec![Settle::new(0, 100), Settle::new(1, 100)]; let params = SettleParams { addr: test_game_addr(), settles: settles.clone(), @@ -249,6 +249,7 @@ mod tests { checkpoint: CheckpointOnChain::default(), settle_version: 0, next_settle_version: 1, + entry_lock: None, }; transport.settle_game(params.clone()).await.unwrap(); transport.settle_game(params.clone()).await.unwrap(); @@ -271,6 +272,7 @@ mod tests { checkpoint: CheckpointOnChain::default(), settle_version: 0, next_settle_version: 1, + entry_lock: None, }; assert_eq!(transport.settle_game(params).await.is_err(), true); } diff --git a/transactor/src/component/event_loop.rs b/transactor/src/component/event_loop.rs index c037c75f..1ededcc8 100644 --- a/transactor/src/component/event_loop.rs +++ b/transactor/src/component/event_loop.rs @@ -1,7 +1,6 @@ use race_api::types::GameDeposit; use async_trait::async_trait; -use race_api::error::Error; use race_api::event::Event; use race_core::context::GameContext; use tracing::{error, info, warn}; @@ -11,7 +10,7 @@ use crate::component::event_bus::CloseReason; use crate::component::wrapped_handler::WrappedHandler; use crate::frame::EventFrame; use crate::utils::current_timestamp; -use race_core::types::{ClientMode, GameAccount, GameMode, GamePlayer}; +use race_core::types::{ClientMode, GameMode, GamePlayer}; use super::ComponentEnv; @@ -25,12 +24,6 @@ pub struct EventLoopContext { game_mode: GameMode, } -pub trait WrappedGameHandler: Send { - fn init(&mut self, init_state: GameAccount) -> Result<(), Error>; - - fn handle_event(&mut self, event: EventFrame) -> Result, Error>; -} - pub struct EventLoop {} #[async_trait] @@ -58,21 +51,21 @@ impl Component for EventLoop { settle_version, .. } => { - if let Some(close_reason) = event_handler::init_state( - init_account, - access_version, - settle_version, - &mut handler, - &mut game_context, - &ports, - ctx.client_mode, - ctx.game_mode, - &env, - ) - .await - { - ports.send(EventFrame::Shutdown).await; - return close_reason; + if game_context.checkpoint_is_empty() { + if let Some(close_reason) = event_handler::init_state( + init_account, + access_version, + settle_version, + &mut handler, + &mut game_context, + &ports, + ctx.client_mode, + ctx.game_mode, + &env, + ).await { + ports.send(EventFrame::Shutdown).await; + return close_reason; + } } } diff --git a/transactor/src/component/event_loop/event_handler.rs b/transactor/src/component/event_loop/event_handler.rs index 018f8256..394d02f3 100644 --- a/transactor/src/component/event_loop/event_handler.rs +++ b/transactor/src/component/event_loop/event_handler.rs @@ -121,11 +121,10 @@ async fn launch_sub_game( let SubGame { bundle_addr, id, - mut init_account, + init_account, } = sub_game; // Use the existing checkpoint when possible. let checkpoint = cp.clone(); - init_account.checkpoint = cp.get_data(id); let settle_version = cp.get_version(id); let ef = EventFrame::LaunchSubGame { @@ -155,12 +154,8 @@ pub async fn init_state( game_mode: GameMode, env: &ComponentEnv, ) -> Option { - if let Err(e) = game_context.set_versions(access_version, settle_version) { - error!("{} Failed to apply checkpoint: {:?}, context settle version: {}, init account settle version: {}", env.log_prefix, e, - game_context.settle_version(), settle_version); - ports.send(EventFrame::Shutdown).await; - return Some(CloseReason::Fault(e)); - } + + let original_versions = game_context.versions(); let effects = match handler.init_state(&mut game_context, &init_account) { Ok(effects) => effects, @@ -172,16 +167,21 @@ pub async fn init_state( } }; - let state_sha = digest(game_context.get_handler_state_raw()); + let EventEffects { checkpoint, launch_sub_games, .. } = effects; + info!( "{} Initialize game state, access_version: {}, settle_version: {}, SHA: {}", - env.log_prefix, access_version, settle_version, state_sha + env.log_prefix, access_version, settle_version, game_context.state_sha() ); + if let Some(checkpoint) = checkpoint { + send_settlement(checkpoint, vec![], vec![], None, original_versions, &game_context, ports, env).await; + } + game_context.dispatch_safe(Event::Ready, 0); if game_mode == GameMode::Main { - launch_sub_game(effects.launch_sub_games, game_context, ports, env).await; + launch_sub_game(launch_sub_games, game_context, ports, env).await; } return None; } diff --git a/transport/src/facade.rs b/transport/src/facade.rs index 448f2c84..2bc691e4 100644 --- a/transport/src/facade.rs +++ b/transport/src/facade.rs @@ -185,12 +185,13 @@ impl TransportT for FacadeTransport { } async fn settle_game(&self, params: SettleParams) -> Result { + let game_addr = params.addr.clone(); let signature = self.client .request("settle", rpc_params![params]) .await .map_err(|e| Error::RpcError(e.to_string()))?; - let game_account = self.get_game_account(&self.addr).await.unwrap().unwrap(); + let game_account = self.get_game_account(&game_addr).await.unwrap().unwrap(); return Ok(SettleResult { signature, diff --git a/transport/src/solana.rs b/transport/src/solana.rs index 7912cfca..d9b78ac0 100755 --- a/transport/src/solana.rs +++ b/transport/src/solana.rs @@ -1596,7 +1596,7 @@ mod tests { }) .await?; let game = transport - .get_game_account(&game_addr, mode) + .get_game_account(&game_addr) .await? .expect("Failed to get game"); assert_eq!(game.players.len(), 2);