diff --git a/apps/web/src/app/@modal/(.)create/page.tsx b/apps/web/src/app/@modal/(.)create/page.tsx index ee7fdae..c79fbca 100644 --- a/apps/web/src/app/@modal/(.)create/page.tsx +++ b/apps/web/src/app/@modal/(.)create/page.tsx @@ -34,7 +34,7 @@ const INPUT_FIELDS = [ { label: "Maximum Players", name: "maxPlayers" }, { label: "Minimum Buy-in", name: "minBuyIn" }, { label: "Maximum Buy-in", name: "maxBuyIn" }, - { label: "Waiting Blocks", name: "waitingBlocks" }, + { label: "Waiting timeOut(seconds)", name: "waitingTimeOut" }, ]; export default function GameCreateModal() { @@ -64,9 +64,9 @@ export default function GameCreateModal() { formValues.smallBlind, formValues.numberOfRaises, formValues.minPlayers, - formValues.maxPlayers, formValues.minBuyIn, formValues.maxBuyIn, + formValues.waitingTimeOut, formValues.chipUnit, 1000, account!.address, diff --git a/apps/web/src/app/games/[id]/state/actions/gameActions.ts b/apps/web/src/app/games/[id]/state/actions/gameActions.ts index ffb6ce1..77eae62 100644 --- a/apps/web/src/app/games/[id]/state/actions/gameActions.ts +++ b/apps/web/src/app/games/[id]/state/actions/gameActions.ts @@ -47,9 +47,9 @@ export const initGame = async ( setGameEventListeners(finalGame); const entryGameState = finalGame.gameState; if (!entryGameState) throw Error("should have existed"); - state$.gameState.players.set(entryGameState.seats.map((p) => p)); + state$.gameState.players.set(entryGameState.players.map((p) => p)); state$.gameState.status.set(entryGameState.status); - state$.gameState.dealer.set(entryGameState.seats[entryGameState.dealerIndex]!); + state$.gameState.dealer.set(entryGameState.players[entryGameState.dealerIndex]!); state$.loading.set(false); //state$.initializing.set(false); }; diff --git a/packages/ts-sdk/package.json b/packages/ts-sdk/package.json index f3010fb..6e96f5f 100644 --- a/packages/ts-sdk/package.json +++ b/packages/ts-sdk/package.json @@ -42,7 +42,6 @@ "@aptos-labs/ts-sdk": "^1.28.0", "@jeton/zk-deck": "*", "events": "^3.3.0", - "graffle": "^0.0.0", "graphql": "^16.9.0", "graphql-request": "^7.1.0", "piesocket-js": "^5.1.0" diff --git a/packages/ts-sdk/src/Jeton/Jeton.ts b/packages/ts-sdk/src/Jeton/Jeton.ts index 6477a60..c891d96 100644 --- a/packages/ts-sdk/src/Jeton/Jeton.ts +++ b/packages/ts-sdk/src/Jeton/Jeton.ts @@ -1,22 +1,26 @@ import { EventEmitter } from "events"; import type { ZKDeck } from "@jeton/zk-deck"; import { - type OnChainDataSource, type OnChainDataSourceInstance, OnChainEventTypes, type OnChainPlayerCheckedInData, + type OnChainTableObject, + type OnChainShuffledDeckData, } from "@src/OnChainDataSource"; import { AptosOnChainDataSource } from "@src/OnChainDataSource/AptosOnChainDataSource"; -import onChainDataMapper from "@src/OnChainDataSource/onChainDataMapper"; -import type { - ChipUnits, - GameEventMap, +import onChainDataMapper, { covertTableInfo } from "@src/OnChainDataSource/onChainDataMapper"; +import { + GameEventTypes, + type ChipUnits, + type GameEventMap, GameStatus, - PlacingBettingActions, - Player, - TableInfo, + type PlacingBettingActions, + type Player, + type TableInfo, } from "@src/types"; import { createLocalZKDeck } from "@src/utils/createZKDeck"; +import { type PendingMemo, createPendingMemo } from "@src/utils/PendingMemo"; +import { hexStringToUint8Array } from "@src/utils/unsignedInt"; export type ZkDeckUrls = { shuffleEncryptDeckWasm: string; @@ -33,10 +37,8 @@ export type JetonConfigs = { secretKey?: bigint; }; -export type Seats = [Player | null]; - export interface JGameState { - seats: Seats; + players: Player[]; dealerIndex: number; status: GameStatus; } @@ -50,6 +52,7 @@ export class Jeton extends EventEmitter { private publicKey: Uint8Array; public gameState?: JGameState; public mySeatIndex?: number; + private pendingMemo: PendingMemo; constructor(config: JetonConfigs) { super(); @@ -61,35 +64,46 @@ export class Jeton extends EventEmitter { this.zkDeck = config.zkDeck; this.secretKey = config.secretKey ?? this.zkDeck.sampleSecretKey(); this.publicKey = this.zkDeck.generatePublicKey(this.secretKey); + + this.pendingMemo = createPendingMemo(); } async checkIn(buyInAmount: number) { console.log("check in, tableInfo", this.tableInfo); - const rawState = await this.onChainDataSource.queryGameState(this.tableInfo.id, ["seets"]); - const seats = onChainDataMapper.convertSeats(rawState.seets); - const alreadyCheckedIn = this.isAlreadyCheckedIn(seats); + const rawState = await this.onChainDataSource.queryGameState(this.tableInfo.id); + const gameState = onChainDataMapper.convertJetonState(rawState); + const alreadyCheckedIn = this.isAlreadyCheckedIn(gameState.players); + console.log("is already checked in?", alreadyCheckedIn, " game state:", gameState); if (!alreadyCheckedIn) { await this.onChainDataSource.checkIn(this.tableInfo.id, buyInAmount, this.publicKey); } - const rawState2 = await this.onChainDataSource.queryGameState(this.tableInfo.id, [ - "seets", - "dealer_index", - "phase", - "time_out", - ]); - this.gameState = onChainDataMapper.convertJetonState(rawState2) as JGameState; - console.log("game state", this.gameState); + const updatedRawState = await this.onChainDataSource.queryGameState(this.tableInfo.id); + this.gameState = onChainDataMapper.convertJetonState(updatedRawState); this.setMySeatIndex(); - console.log("set seat index"); + console.log("my seat index is", this.mySeatIndex); this.addOnChainListeners(); + this.checkForActions(updatedRawState); + this.createTableEvents(updatedRawState); + } + + private createTableEvents(onChainTableObject: OnChainTableObject) { + const shufflingPlayerIndex = + onChainTableObject.state.__variant__ === "Playing" && + onChainTableObject.state.phase.__variant__ === "Shuffle" && + onChainTableObject.state.phase.turn_index; + if (shufflingPlayerIndex === false) { + console.log("shuffle ended, lets do private card decryption"); + return; + } + const shufflingPlayer = onChainTableObject.roster.players[shufflingPlayerIndex]!; + this.emit(GameEventTypes.PLAYER_SHUFFLING, onChainDataMapper.convertPlayer(shufflingPlayer)); } private addOnChainListeners() { this.onChainDataSource.on(OnChainEventTypes.PLAYER_CHECKED_IN, this.newPlayerCheckedIn); - // this.onChainDataSource.on(OnChainEventTypes.GAME_STARTED, this.gameStarted); - // this.onChainDataSource.on(OnChainEventTypes.SHUFFLED_DECK, this.playerShuffledDeck); + this.onChainDataSource.on(OnChainEventTypes.SHUFFLED_DECK, this.playerShuffledDeck); // this.onChainDataSource.on( // OnChainEventTypes.PRIVATE_CARDS_SHARES_RECEIVED, // this.receivedPrivateCardsShares, @@ -99,29 +113,99 @@ export class Jeton extends EventEmitter { // this.receivedPublicCardsShares, // ); // this.onChainDataSource.on(OnChainEventTypes.PLAYER_PLACED_BET, this.receivedPlayerBet); + this.onChainDataSource.listenToTableEvents(this.tableInfo.id); } - private newPlayerCheckedIn = (data: OnChainPlayerCheckedInData) => { + private newPlayerCheckedIn = async (data: OnChainPlayerCheckedInData) => { + if (!this.gameState) throw new Error("game state must exist"); console.log("new player checked in", data); + const onChainTableObject = await this.pendingMemo.memoize(this.queryGameState); + const newJState = onChainDataMapper.convertJetonState(onChainTableObject); + const newPlayer = newJState.players.find((p) => p.id === data.address); + if (!newPlayer) throw new Error("new player must have existed!!"); + // TODO: are you sure? you want to replace all the players? + // this.sendUIevents(); + this.emit(GameEventTypes.NEW_PLAYER_CHECK_IN, newPlayer); + // this.takeAction(); + if ( + newJState.status === GameStatus.Shuffle && + this.gameState.status === GameStatus.AwaitingStart + ) { + const dealerIndex = newJState.dealerIndex; + //@ts-ignore + const shufflingPlayer = onChainTableObject.state?.phase?.turn_index; + this.emit(GameEventTypes.HAND_STARTED, { dealer: newJState.players[dealerIndex]! }); + this.emit(GameEventTypes.PLAYER_SHUFFLING, newJState.players[shufflingPlayer]!); + this.checkForActions(onChainTableObject); + } + + // this.syncState(newGameState); + this.gameState = newJState; }; + private async playerShuffledDeck(data: OnChainShuffledDeckData) { + const onChainTableObject = await this.pendingMemo.memoize(this.queryGameState); + const shufflingPlayerIndex = + onChainTableObject.state.__variant__ === "Playing" && + onChainTableObject.state.phase.__variant__ === "Shuffle" && + onChainTableObject.state.phase.turn_index; + if (shufflingPlayerIndex === false) { + console.log("shuffle ended, lets do private card decryption"); + return; + } + const shufflingPlayer = onChainTableObject.roster.players[shufflingPlayerIndex]!; + this.emit(GameEventTypes.PLAYER_SHUFFLING, onChainDataMapper.convertPlayer(shufflingPlayer)); + this.checkForActions(onChainTableObject); + } + + private checkForActions(onChainTableObject: OnChainTableObject) { + if ( + onChainTableObject.state.__variant__ === "Playing" && + onChainTableObject.state.phase.__variant__ === "Shuffle" && + onChainTableObject.roster.players[onChainTableObject.state.phase.turn_index]?.addr === + this.playerId + ) { + const publicKeys = onChainTableObject.roster.players.map((p) => + hexStringToUint8Array(p.public_key), + ); + this.shuffle(hexStringToUint8Array(onChainTableObject.state.deck), publicKeys); + } + } + + private async shuffle(deck: Uint8Array, publicKeys: Uint8Array[]) { + console.log("going to shuffle deck"); + if (!this.zkDeck) throw new Error("zkDeck must have been created by now!"); + if (!this.gameState) throw new Error("game state must be present"); + const aggregatedPublicKey = this.zkDeck.generateAggregatedPublicKey(publicKeys); + const { proof, outputDeck } = await this.zkDeck.proveShuffleEncryptDeck( + aggregatedPublicKey, + deck, + ); + console.log("shuffled deck", outputDeck); + this.onChainDataSource.shuffledDeck(this.tableInfo.id, outputDeck, proof); + } + private setMySeatIndex() { if (!this.gameState) throw new Error("No game state"); - for (const [seat, player] of Object.entries(this.gameState.seats)) { + for (const [index, player] of Object.entries(this.gameState.players)) { if (player && player.id === this.playerId) { - this.mySeatIndex = Number(seat); + this.mySeatIndex = Number(index); return; } } } - private isAlreadyCheckedIn(seats: Seats): boolean { - for (const player of seats) { + private isAlreadyCheckedIn(players: Player[]): boolean { + for (const player of players) { if (player && player.id === this.playerId) return true; } return false; } + private queryGameState = () => { + return this.onChainDataSource.queryGameState(this.tableInfo.id); + }; + public placeBet(action: PlacingBettingActions) { console.log("placeBet Called", action); } @@ -157,7 +241,7 @@ export class Jeton extends EventEmitter { accountAddress, signAndSubmitTransaction, ); - const tableInfo = await onChainDataSource.createTable( + const [tableAddress, tableObject] = await onChainDataSource.createTable( smallBlind, numberOfRaises, minPlayers, @@ -168,6 +252,7 @@ export class Jeton extends EventEmitter { chipUnit, publicKey, ); + const tableInfo = covertTableInfo(tableAddress, tableObject); const jeton = new Jeton({ tableInfo, diff --git a/packages/ts-sdk/src/OnChainDataSource/AptosOnChainDataSource.ts b/packages/ts-sdk/src/OnChainDataSource/AptosOnChainDataSource.ts index d313639..c32e967 100644 --- a/packages/ts-sdk/src/OnChainDataSource/AptosOnChainDataSource.ts +++ b/packages/ts-sdk/src/OnChainDataSource/AptosOnChainDataSource.ts @@ -1,25 +1,33 @@ import { EventEmitter } from "events"; -import type { - OnChainDataSource, - OnChainDataSourceInstance, - OnChainEventMap, - OnChainTableObject, +import { + OnChainEventTypes, + type OnChainDataSource, + type OnChainDataSourceInstance, + type OnChainEventMap, + type OnChainTableObject, } from "@src/OnChainDataSource"; import { createTableInfo } from "@src/contracts/contractDataMapper"; import { callCheckInContract, + callShuffleEncryptDeck, createTableObject, getTableObject, getTableObjectAddresses, + queryEvents, } from "@src/contracts/contractInteractions"; import type { ChipUnits, TableInfo } from "@src/types"; -// @ts-ignore -import { gql, request } from "graffle"; +import { POLLING_INTERVAL } from "./constants"; +import { contractCheckedInEventType } from "@src/contracts/contractData"; export class AptosOnChainDataSource extends EventEmitter implements OnChainDataSourceInstance { + pollingTables: Record< + string, + { timerId: number | NodeJS.Timeout; lastEventBlockHeight?: number } + > = {}; + constructor( public address: string, // biome-ignore lint/suspicious/noExplicitAny: @@ -29,6 +37,56 @@ export class AptosOnChainDataSource this.address = address; } + private publishEvent(event: { data: { sender_addr: string }; indexed_type: string }) { + console.log("publish event", event); + switch (event.indexed_type) { + case contractCheckedInEventType: + console.log("publishing", OnChainEventTypes.PLAYER_CHECKED_IN); + this.emit(OnChainEventTypes.PLAYER_CHECKED_IN, { address: event.data.sender_addr }); + break; + } + } + + private pollTableEvents = async (tableId: string) => { + const events = await queryEvents(tableId); + const lastEventIndex = this.pollingTables[tableId]!.lastEventBlockHeight; + if (lastEventIndex) { + const newEvents = events + .filter((ev) => ev.transaction_block_height > lastEventIndex) + .reverse(); + console.log("new events are", newEvents); + for (const event of newEvents) { + this.publishEvent(event); + } + } + console.log( + "events are:", + events, + this.pollingTables[tableId]!, + events[0], + events[0].transaction_block_height, + tableId, + ); + // TODO: parse events and emit + const pollingTable = this.pollingTables[tableId]; + if (pollingTable) { + pollingTable.lastEventBlockHeight = events[0].transaction_block_height; + pollingTable.timerId = setTimeout(this.pollTableEvents.bind(this, tableId), POLLING_INTERVAL); + } + }; + + public listenToTableEvents(tableId: string) { + console.log("listen to events", tableId); + if (this.pollingTables[tableId]) return; + const timerId = setTimeout(this.pollTableEvents.bind(this, tableId), POLLING_INTERVAL); + this.pollingTables[tableId] = { timerId: timerId }; + } + + public disregardTableEvents(tableId: string) { + if (!this.pollingTables[tableId]) return; + clearInterval(this.pollingTables[tableId]?.timerId); + } + public async createTable( smallBlind: number, // number of raises allowed in one round of betting @@ -45,7 +103,8 @@ export class AptosOnChainDataSource chipUnit: ChipUnits, publicKey: Uint8Array, ) { - const [tableAddress, tableResourceObject] = await createTableObject( + return await createTableObject( + waitingTimeOut, smallBlind, numberOfRaises, minPlayers, @@ -56,16 +115,18 @@ export class AptosOnChainDataSource publicKey, this.singAndSubmitTransaction, ); - const tableInfo = createTableInfo(tableAddress, tableResourceObject); - return tableInfo; } - async queryGameState( + queryGameState(id: string): Promise; + // TODO: partial query + queryGameState( id: string, fields: T[], + ): Promise>; + async queryGameState( + id: string, + fields?: T[], ): Promise> { - console.log("query game state"); - //TODO const tableObject = (await getTableObject(id)) as OnChainTableObject; console.log("OnChainTableObject", tableObject); return tableObject; @@ -82,22 +143,14 @@ export class AptosOnChainDataSource ); } - async queryEvents(tableId: string) { - const document = gql` - query MyQuery { - events( - where: {indexed_type: {_eq: "${tableId}::texas_holdem::TableCreatedEvent"}, data: {_cast: {String: {_like: "%0x73639065d084db4d47531fcae25da3fd003ae43dec691f53a7ef5dcce072676c%"}}}} - ) { - data, - indexed_type - } -} -`; - const res = await request( - "https://aptos-testnet.nodit.io/tUOKeLdo0yUmJsNgwfln97h_03wYs8mP/v1/graphql", - document, + async shuffledDeck(tableId: string, outDeck: Uint8Array, proof: Uint8Array) { + return await callShuffleEncryptDeck( + this.address, + outDeck, + proof, + tableId, + this.singAndSubmitTransaction, ); - console.log("res is", res); } static async getTableInfo(id: string) { diff --git a/packages/ts-sdk/src/OnChainDataSource/OnChainDataSource.ts b/packages/ts-sdk/src/OnChainDataSource/OnChainDataSource.ts index 5b551aa..1b709e7 100644 --- a/packages/ts-sdk/src/OnChainDataSource/OnChainDataSource.ts +++ b/packages/ts-sdk/src/OnChainDataSource/OnChainDataSource.ts @@ -1,7 +1,7 @@ import type EventEmitter from "events"; import type { ChipUnits, TableInfo } from "../types/Table"; import type { OnChainEventMap } from "./onChainEvents.types"; -import type { OnChainTableObject } from "./onChainObjects.types"; +import type { OnChainTableObject, TableAddress } from "./onChainObjects.types"; export interface OnChainDataSource { new ( @@ -29,12 +29,16 @@ export interface OnChainDataSourceInstance extends EventEmitter waitingTimeOut: number, chipUnit: ChipUnits, publicKey: Uint8Array, - ): Promise; + ): Promise<[TableAddress, OnChainTableObject]>; + listenToTableEvents(tableId: string): void; + disregardTableEvents(tableId: string): void; queryGameState( id: string, fields: T[], ): Promise>; + queryGameState(id: string): Promise; checkIn(tableId: string, buyInAmount: number, publicKey: Uint8Array): Promise; + shuffledDeck(tableId: string, outDeck: Uint8Array, proof: Uint8Array): Promise; } diff --git a/packages/ts-sdk/src/OnChainDataSource/constants.ts b/packages/ts-sdk/src/OnChainDataSource/constants.ts new file mode 100644 index 0000000..cb90f49 --- /dev/null +++ b/packages/ts-sdk/src/OnChainDataSource/constants.ts @@ -0,0 +1 @@ +export const POLLING_INTERVAL = 3 * 1000; diff --git a/packages/ts-sdk/src/OnChainDataSource/onChainDataMapper.ts b/packages/ts-sdk/src/OnChainDataSource/onChainDataMapper.ts index b09086a..821a7b9 100644 --- a/packages/ts-sdk/src/OnChainDataSource/onChainDataMapper.ts +++ b/packages/ts-sdk/src/OnChainDataSource/onChainDataMapper.ts @@ -1,54 +1,107 @@ -import type { OnChainPhase, OnChainPlayer, OnChainTableObject } from "@src/OnChainDataSource"; -import type { JGameState, Seats } from "../Jeton/Jeton"; +import type { + ChipStack, + OnChainActivePlayer, + OnChainGameState, + OnChainPendingPlayer, + OnChainPhase, + OnChainTableObject, +} from "@src/OnChainDataSource"; +import type { JGameState } from "../Jeton/Jeton"; import { GameStatus, type Player, PlayerStatus } from "../types"; +import { ChipUnits, type TableInfo } from "@src/types"; +import { getTableObject } from "@src/contracts/contractInteractions"; + +function isActive( + player: OnChainActivePlayer | OnChainPendingPlayer, +): player is OnChainActivePlayer { + return (player as OnChainActivePlayer).bet != null; +} + +export const chipStackToNumber = (stack: ChipStack) => { + return Number(stack._0); +}; + +export const covertTableInfo = ( + tableObjectAddress: string, + tableObjectResource: OnChainTableObject, +): TableInfo => { + //TODO check maxPlayers and minPlayers value + const tableInfo: TableInfo = { + id: tableObjectAddress, + smallBlind: Number(tableObjectResource.config.small_bet), + numberOfRaises: Number(tableObjectResource.config.num_raises), + minPlayers: Number(tableObjectResource.config.start_at_player), + maxPlayers: Number(tableObjectResource.config.max_players), + minBuyIn: Number(tableObjectResource.config.min_buy_in_amount), + maxBuyIn: Number(tableObjectResource.config.max_buy_in_amount), + chipUnit: ChipUnits.apt, + waitingTimeout: Number(tableObjectResource.config.action_timeout), + }; + return tableInfo; +}; + +const convertPlayer = (player: OnChainActivePlayer | OnChainPendingPlayer): Player => { + if (isActive(player)) + return { + id: player.addr, + balance: chipStackToNumber(player.remaining), + bet: chipStackToNumber(player.bet), + status: player.is_folded + ? PlayerStatus.folded + : Number(player.remaining) === 0 + ? PlayerStatus.allIn + : PlayerStatus.active, + }; -const convertPlayer = (player: OnChainPlayer): Player => { return { id: player.addr, - balance: Number(player.balance), - bet: Number(player.bet), - status: player.is_folded - ? PlayerStatus.folded - : Number(player.balance) === 0 - ? PlayerStatus.allIn - : player.is_playing - ? PlayerStatus.active - : PlayerStatus.sittingOut, - elGamalPublicKey: Uint8Array.from(player.public_key), + balance: chipStackToNumber(player.balance), + bet: 0, + status: PlayerStatus.sittingOut, }; }; -const convertSeats = (seats: OnChainTableObject["seets"]): Seats => { +const convertPlayers = (players: OnChainActivePlayer[], waitings: OnChainPendingPlayer[]) => { // TODO: - //@ts-ignore - const transformedSeats: Seats = []; - for (const seat of seats) { - const player = seat.vec[0]; - if (player) { - transformedSeats.push(convertPlayer(player)); - } else { - transformedSeats.push(null); - } + const finalPlayers: Player[] = []; + for (const player of players) { + finalPlayers.push(convertPlayer(player)); } - return transformedSeats; + for (const player of waitings) { + finalPlayers.push(convertPlayer(player)); + } + + return finalPlayers; }; -const convertGameStatus = (phase: OnChainPhase) => { - // TODO: - return GameStatus.AwaitingStart as GameStatus; +const convertGameStatus = (state: OnChainGameState) => { + switch (state.__variant__) { + case "AwaitingStart": + case "Removed": + return GameStatus.AwaitingStart; + case "Playing": { + //TODO: + return GameStatus.Shuffle; + } + } +}; + +const getDealerIndex = (tableObject: OnChainTableObject) => { + const smallIndex = tableObject.roster.small_index; + const numActivePlayers = tableObject.roster.players.length; + const dealerIndex = smallIndex === 0 ? numActivePlayers - 1 : smallIndex - 1; + return dealerIndex; }; -const convertJetonState = ( - state: Pick, -): JGameState => { +const convertJetonState = (state: OnChainTableObject): JGameState => { // TODO: - console.log("state is", state); + const gameStatus = convertGameStatus(state.state); return { - dealerIndex: state.dealer_index, - seats: convertSeats(state.seets), - status: convertGameStatus(state.phase), + dealerIndex: gameStatus === GameStatus.AwaitingStart ? 0 : getDealerIndex(state), + players: convertPlayers(state.roster.players, state.roster.waitings), + status: gameStatus, }; }; -export default { convertSeats, convertJetonState }; +export default { convertPlayer, convertPlayers, convertJetonState }; diff --git a/packages/ts-sdk/src/OnChainDataSource/onChainEvents.types.ts b/packages/ts-sdk/src/OnChainDataSource/onChainEvents.types.ts index 290f34b..693623b 100644 --- a/packages/ts-sdk/src/OnChainDataSource/onChainEvents.types.ts +++ b/packages/ts-sdk/src/OnChainDataSource/onChainEvents.types.ts @@ -2,7 +2,6 @@ import type { BettingActions, BettingRounds, PublicCardRounds } from "../types"; export enum OnChainEventTypes { PLAYER_CHECKED_IN = "player-checked-in", - GAME_STARTED = "game-started", SHUFFLED_DECK = "shuffled-deck", PRIVATE_CARDS_SHARES_RECEIVED = "private-cards-shares", PLAYER_PLACED_BET = "player-placed-bet", @@ -13,10 +12,6 @@ export type OnChainPlayerCheckedInData = { address: string; }; -export type OnChainGameStartedData = { - dealerIndex: number; -}; - export type OnChainShuffledDeckData = { address: string; }; @@ -38,7 +33,6 @@ export type OnChainPublicCardsSharesData = { export type OnChainEventMap = { [OnChainEventTypes.PLAYER_CHECKED_IN]: [OnChainPlayerCheckedInData]; - [OnChainEventTypes.GAME_STARTED]: [OnChainGameStartedData]; [OnChainEventTypes.SHUFFLED_DECK]: [OnChainShuffledDeckData]; [OnChainEventTypes.PRIVATE_CARDS_SHARES_RECEIVED]: [OnChainPrivateCardsSharesData]; [OnChainEventTypes.PLAYER_PLACED_BET]: [OnChainPlayerPlacedBetData]; diff --git a/packages/ts-sdk/src/OnChainDataSource/onChainObjects.types.ts b/packages/ts-sdk/src/OnChainDataSource/onChainObjects.types.ts index 80b9f97..288f2b1 100644 --- a/packages/ts-sdk/src/OnChainDataSource/onChainObjects.types.ts +++ b/packages/ts-sdk/src/OnChainDataSource/onChainObjects.types.ts @@ -1,44 +1,72 @@ -export type OnChainPlayer = { +export type TableAddress = string; + +export type ChipStack = { + _0: string; +}; + +export type OnChainActivePlayer = { addr: string; - public_key: Uint8Array; - balance: number; - bet: number; + remaining: ChipStack; + bet: ChipStack; + stake: ChipStack; is_folded: boolean; - is_playing: boolean; is_last_hand: boolean; + public_key: string; +}; + +export type OnChainPendingPlayer = { + addr: string; + balance: ChipStack; + stake: ChipStack; + public_key: string; }; export type DrawPrivateCardsPhase = { - type: "DrawPrivateCards"; + __variant__: "DrawPrivateCards"; contributors_index: number[]; }; export type ShufflePhase = { - type: "Shuffle"; - seat_index: number; + __variant__: "Shuffle"; + turn_index: number; }; -export type AwaitingStartPhase = { - type: "AwaitingStart"; +export type OnChainPhase = ShufflePhase | DrawPrivateCardsPhase; + +export type AwaitingStartState = { + __variant__: "AwaitingStart"; }; -export type OnChainPhase = AwaitingStartPhase | ShufflePhase | DrawPrivateCardsPhase; +export type RemovedState = { + __variant__: "Removed"; +}; -export type OnChainTableObject = { - seets: ({ vec: [OnChainPlayer] } | { vec: [] })[]; - dealer_index: number; +export type PlayingState = { + __variant__: "Playing"; + timeout: string; + deck: string; + // TODO: what does it represent? + decryption_card_shares: Uint8Array[]; phase: OnChainPhase; - time_out: number; - aggregated_public_key: Uint8Array[]; - deck: Uint8Array[]; - private_cards_share: Uint8Array[][]; - public_cards_share: Uint8Array[][]; - info: { +}; + +export type OnChainGameState = AwaitingStartState | PlayingState | RemovedState; +export type OnChainTableObject = { + state: OnChainGameState; + roster: { + stake_amount: number; + max_players: number; + small_index: number; + waitings: OnChainPendingPlayer[]; + players: OnChainActivePlayer[]; + }; + config: { action_timeout: number; max_buy_in_amount: number; min_buy_in_amount: number; num_raises: number; - small_blind: number; + small_bet: number; start_at_player: number; + max_players: number; }; }; diff --git a/packages/ts-sdk/src/contracts/contractData.ts b/packages/ts-sdk/src/contracts/contractData.ts index da0ca3e..2b7805a 100644 --- a/packages/ts-sdk/src/contracts/contractData.ts +++ b/packages/ts-sdk/src/contracts/contractData.ts @@ -1,12 +1,14 @@ import type { MoveStructId } from "@aptos-labs/ts-sdk"; export const contractAddress = process.env.NEXT_PUBLIC_CONTRACTS_ADDR as string; -const appName = "texas_holdem"; -const tableCreatedEvent = "TableCreatedEvent"; +const appName = "holdem_table"; const tableType = "Table"; export const contractTableCreatedEventType = - `${contractAddress}::${appName}::${tableCreatedEvent}` as MoveStructId; + `${contractAddress}::${appName}::TableCreatedEvent` as MoveStructId; +export const contractCheckedInEventType = + `${contractAddress}::${appName}::CheckedInEvent` as MoveStructId; + export const contractTableType = `${contractAddress}::${appName}::${tableType}` as MoveStructId; const createTable = "create_table"; @@ -14,3 +16,8 @@ export const contractCreateTableFunctionName = `${contractAddress}::${appName}::${createTable}` as MoveStructId; export const contractCheckInFunctionName = `${contractAddress}::${appName}::check_in` as MoveStructId; +export const contractShuffleEncryptDeckFunctionName = + `${contractAddress}::${appName}::shuffle_encrypt_deck` as MoveStructId; + +export const NODIT_GQL_ADDRESS = + "https://aptos-testnet.nodit.io/tUOKeLdo0yUmJsNgwfln97h_03wYs8mP/v1/graphql"; diff --git a/packages/ts-sdk/src/contracts/contractDataMapper.ts b/packages/ts-sdk/src/contracts/contractDataMapper.ts index 9a1fc08..7fb41ac 100644 --- a/packages/ts-sdk/src/contracts/contractDataMapper.ts +++ b/packages/ts-sdk/src/contracts/contractDataMapper.ts @@ -1,21 +1,19 @@ import { ChipUnits, type TableInfo } from "@src/types"; - -// biome-ignore lint/suspicious/noExplicitAny: -type Table = any; +import type { OnChainTableObject } from "../OnChainDataSource"; export const createTableInfo = ( tableObjectAddress: string, - tableObjectResource: Table, + tableObjectResource: OnChainTableObject, ): TableInfo => { //TODO check maxPlayers and minPlayers value const tableInfo: TableInfo = { id: tableObjectAddress, - smallBlind: tableObjectResource.info.small_blind, - numberOfRaises: tableObjectResource.info.num_raises, - minPlayers: tableObjectResource.info.start_at_player, - maxPlayers: tableObjectResource.start_at_player + 4, - minBuyIn: tableObjectResource.info.min_buy_in_amount, - maxBuyIn: tableObjectResource.info.max_buy_in_amount, + smallBlind: Number(tableObjectResource.config.small_bet), + numberOfRaises: Number(tableObjectResource.config.num_raises), + minPlayers: Number(tableObjectResource.config.start_at_player), + maxPlayers: Number(tableObjectResource.config.max_players), + minBuyIn: Number(tableObjectResource.config.min_buy_in_amount), + maxBuyIn: Number(tableObjectResource.config.max_buy_in_amount), chipUnit: ChipUnits.apt, }; return tableInfo; diff --git a/packages/ts-sdk/src/contracts/contractInteractions.ts b/packages/ts-sdk/src/contracts/contractInteractions.ts index c2432ce..6317088 100644 --- a/packages/ts-sdk/src/contracts/contractInteractions.ts +++ b/packages/ts-sdk/src/contracts/contractInteractions.ts @@ -1,11 +1,14 @@ import type { GetEventsResponse } from "@aptos-labs/ts-sdk"; +import { gql, request } from "graphql-request"; import type { WriteSetChange, WriteSetChangeWriteResource } from "@aptos-labs/ts-sdk"; -import type { OnChainTableObject } from "@src/OnChainDataSource"; -import { ChipUnits, TableInfo } from "@src/types"; +import type { OnChainTableObject, TableAddress } from "@src/OnChainDataSource"; import { aptos } from "@src/utils/aptos"; import { + NODIT_GQL_ADDRESS, contractCheckInFunctionName, + contractCheckedInEventType, contractCreateTableFunctionName, + contractShuffleEncryptDeckFunctionName, contractTableCreatedEventType, contractTableType, } from "./contractData"; @@ -40,7 +43,7 @@ export const callCheckInContract = async ( sender: address, data: { function: contractCheckInFunctionName, - functionArguments: [tableObjetAddress, publicKey, buyInAmount], + functionArguments: [tableObjetAddress, buyInAmount, publicKey], }, }); const transactionData = await aptos.waitForTransaction({ @@ -49,7 +52,30 @@ export const callCheckInContract = async ( console.log("after tx hash", transactionData); }; +export const callShuffleEncryptDeck = async ( + address: string, + outDeck: Uint8Array, + proof: Uint8Array, + tableObjectAddress: string, + // biome-ignore lint/suspicious/noExplicitAny: + signAndSubmitTransaction: any, +) => { + console.log("call shuffle contract", address, outDeck, proof); + const submittedTransaction = await signAndSubmitTransaction({ + sender: address, + data: { + function: contractShuffleEncryptDeckFunctionName, + functionArguments: [tableObjectAddress, outDeck, proof], + }, + }); + const transactionData = await aptos.waitForTransaction({ + transactionHash: submittedTransaction.hash, + }); + console.log("shuffle transaction result", transactionData); +}; + export const createTableObject = async ( + waitingTimeOut: number, smallBlind: number, numberOfRaises: number, minPlayers: number, @@ -63,21 +89,22 @@ export const createTableObject = async ( // biome-ignore lint/suspicious/noExplicitAny: signAndSubmitTransaction: any, ) => { + console.log("create table args", maxBuyIn, minBuyIn, waitingTimeOut); //TODO format public key before passing const submitCreateTableTransactionHash = await signAndSubmitTransaction({ sender: accountAddress, data: { function: contractCreateTableFunctionName, functionArguments: [ - 2 * 60, + waitingTimeOut || 2 * 60, minBuyIn, maxBuyIn, smallBlind, numberOfRaises, minPlayers, 9, - publicKey, buyInAmount, + publicKey, ], }, }); @@ -92,7 +119,10 @@ export const createTableObject = async ( )! as WriteSetChangeWriteResource; console.log("transaction resource", tableWriteChange); - return [tableWriteChange.address, tableWriteChange.data.data] as const; + return [tableWriteChange.address, tableWriteChange.data.data as OnChainTableObject] as [ + TableAddress, + OnChainTableObject, + ]; }; function isWriteSetChangeWriteResource( @@ -100,3 +130,22 @@ function isWriteSetChangeWriteResource( ): write is WriteSetChangeWriteResource { return (write as WriteSetChangeWriteResource).data !== undefined; } + +export async function queryEvents(tableId: string) { + const document = gql` + query MyQuery { + events( + where: {indexed_type: {_in: ["${contractTableCreatedEventType}", "${contractCheckedInEventType}"]}, + data: {_cast: {String: {_like: "%${tableId}%"}}}}, + order_by: {transaction_block_height: desc} + ) { + data, + indexed_type, + transaction_block_height + } + }`; + //TODO: typing + const res = await request<{ events: any[] }>(NODIT_GQL_ADDRESS, document); + console.log("res is", res); + return res.events; +} diff --git a/packages/ts-sdk/src/types/Player.ts b/packages/ts-sdk/src/types/Player.ts index 36414ce..fd76e51 100644 --- a/packages/ts-sdk/src/types/Player.ts +++ b/packages/ts-sdk/src/types/Player.ts @@ -9,7 +9,6 @@ export enum PlayerStatus { export interface Player { id: string; balance: number; - elGamalPublicKey: ElGamalPublicKey; status: PlayerStatus; bet?: number; } diff --git a/packages/ts-sdk/src/types/Table.ts b/packages/ts-sdk/src/types/Table.ts index 8c160fa..f201995 100644 --- a/packages/ts-sdk/src/types/Table.ts +++ b/packages/ts-sdk/src/types/Table.ts @@ -13,4 +13,5 @@ export interface TableInfo { minBuyIn: number; maxBuyIn: number; chipUnit: ChipUnits; + waitingTimeout: number; // seconds } diff --git a/packages/ts-sdk/src/utils/PendingMemo.ts b/packages/ts-sdk/src/utils/PendingMemo.ts new file mode 100644 index 0000000..343d823 --- /dev/null +++ b/packages/ts-sdk/src/utils/PendingMemo.ts @@ -0,0 +1,17 @@ +export class PendingMemo { + // biome-ignore lint/suspicious/noExplicitAny: + memoizedMap = new Map<() => Promise, Promise>(); + + memoize(asyncFunction: () => Promise) { + if (this.memoizedMap.get(asyncFunction)) + return this.memoizedMap.get(asyncFunction) as Promise; + const promiseRes = asyncFunction().then((value) => { + this.memoizedMap.delete(asyncFunction); + return value; + }); + this.memoizedMap.set(asyncFunction, promiseRes); + return promiseRes; + } +} + +export const createPendingMemo = () => new PendingMemo(); diff --git a/packages/ts-sdk/src/utils/unsignedInt.ts b/packages/ts-sdk/src/utils/unsignedInt.ts new file mode 100644 index 0000000..db96878 --- /dev/null +++ b/packages/ts-sdk/src/utils/unsignedInt.ts @@ -0,0 +1,8 @@ +export function hexStringToUint8Array(rawString: string) { + const s = rawString[1] === "x" ? rawString.slice(2) : rawString; + const res = new Uint8Array(s.length / 2); + for (let i = 0; i < s.length; i += 2) { + res[i / 2] = Number.parseInt(s[i]!, 16) * 16 + Number.parseInt(s[i + 1]!, 16); + } + return res; +}