From 07be70e9a974561a6ffcda5e3bffea5d5f1dc05e Mon Sep 17 00:00:00 2001 From: Vadim Tokar Date: Thu, 7 Nov 2024 11:07:31 +0400 Subject: [PATCH 1/2] Syndote: add sails --- frontend/apps/syndote/README.md | 2 - frontend/apps/syndote/src/app/hooks/index.ts | 3 + .../src/app/hooks/use-execute-with-pendig.ts | 35 + .../apps/syndote/src/app/hooks/use-pending.ts | 8 + .../src/app/hooks/use-sign-and-send.ts | 28 + frontend/apps/syndote/src/app/utils/index.ts | 1 + .../src/app/utils/sails/events/index.ts | 2 + .../use-event-game-canceled-subscription.ts | 20 + .../events/use-event-step-subscription.ts | 14 + .../apps/syndote/src/app/utils/sails/index.ts | 5 + .../src/app/utils/sails/messages/index.ts | 8 + .../use-add-gas-to-player-strategy-message.ts | 30 + .../use-cancel-game-session-message.ts | 30 + .../use-create-game-session-message.ts | 34 + .../sails/messages/use-delete-game-message.ts | 30 + .../messages/use-delete-player-message.ts | 30 + .../sails/messages/use-exit-game-message.ts | 30 + .../utils/sails/messages/use-play-message.ts | 30 + .../sails/messages/use-register-message.ts | 34 + .../src/app/utils/sails/queries/index.ts | 1 + .../queries/use-get-game-session-query.ts | 24 + .../apps/syndote/src/app/utils/sails/sails.ts | 16 + .../syndote/src/app/utils/sails/syndote.idl | 100 +++ .../syndote/src/app/utils/sails/syndote.ts | 624 ++++++++++++++++++ .../src/app/utils/sails/syndote_player.idl | 25 + .../src/app/utils/sails/syndote_player.ts | 105 +++ frontend/apps/syndote/src/atoms.ts | 3 +- .../src/components/layout/header/Header.tsx | 14 +- frontend/apps/syndote/src/hooks/metadata.ts | 99 --- .../apps/syndote/src/hooks/useQuitGame.ts | 53 +- frontend/apps/syndote/src/pages/home/Home.tsx | 260 +++----- .../apps/syndote/src/pages/home/cell/Cell.tsx | 11 +- .../game-finished-modal/GameFinishedModal.tsx | 2 +- .../src/pages/home/players/Players.tsx | 5 +- .../pages/home/session-info/SessionInfo.tsx | 36 +- .../create-game-form/CreateGameForm.tsx | 45 +- .../join-game-form/JoinGameForm.tsx | 63 +- .../components/request-game/RequestGame.tsx | 8 +- frontend/apps/syndote/src/types.ts | 56 +- frontend/apps/syndote/src/utils.ts | 23 + 40 files changed, 1488 insertions(+), 459 deletions(-) create mode 100644 frontend/apps/syndote/src/app/hooks/index.ts create mode 100644 frontend/apps/syndote/src/app/hooks/use-execute-with-pendig.ts create mode 100644 frontend/apps/syndote/src/app/hooks/use-pending.ts create mode 100644 frontend/apps/syndote/src/app/hooks/use-sign-and-send.ts create mode 100644 frontend/apps/syndote/src/app/utils/index.ts create mode 100644 frontend/apps/syndote/src/app/utils/sails/events/index.ts create mode 100644 frontend/apps/syndote/src/app/utils/sails/events/use-event-game-canceled-subscription.ts create mode 100644 frontend/apps/syndote/src/app/utils/sails/events/use-event-step-subscription.ts create mode 100644 frontend/apps/syndote/src/app/utils/sails/index.ts create mode 100644 frontend/apps/syndote/src/app/utils/sails/messages/index.ts create mode 100644 frontend/apps/syndote/src/app/utils/sails/messages/use-add-gas-to-player-strategy-message.ts create mode 100644 frontend/apps/syndote/src/app/utils/sails/messages/use-cancel-game-session-message.ts create mode 100644 frontend/apps/syndote/src/app/utils/sails/messages/use-create-game-session-message.ts create mode 100644 frontend/apps/syndote/src/app/utils/sails/messages/use-delete-game-message.ts create mode 100644 frontend/apps/syndote/src/app/utils/sails/messages/use-delete-player-message.ts create mode 100644 frontend/apps/syndote/src/app/utils/sails/messages/use-exit-game-message.ts create mode 100644 frontend/apps/syndote/src/app/utils/sails/messages/use-play-message.ts create mode 100644 frontend/apps/syndote/src/app/utils/sails/messages/use-register-message.ts create mode 100644 frontend/apps/syndote/src/app/utils/sails/queries/index.ts create mode 100644 frontend/apps/syndote/src/app/utils/sails/queries/use-get-game-session-query.ts create mode 100644 frontend/apps/syndote/src/app/utils/sails/sails.ts create mode 100644 frontend/apps/syndote/src/app/utils/sails/syndote.idl create mode 100644 frontend/apps/syndote/src/app/utils/sails/syndote.ts create mode 100644 frontend/apps/syndote/src/app/utils/sails/syndote_player.idl create mode 100644 frontend/apps/syndote/src/app/utils/sails/syndote_player.ts delete mode 100644 frontend/apps/syndote/src/hooks/metadata.ts diff --git a/frontend/apps/syndote/README.md b/frontend/apps/syndote/README.md index 8e553c4cf..c1734a141 100644 --- a/frontend/apps/syndote/README.md +++ b/frontend/apps/syndote/README.md @@ -24,8 +24,6 @@ yarn install Create `.env` file, `.env.example` will let you know what variables are expected. -Put the latest version of the `syndote.meta.wasm` file locally in `gear-js\apps\syndote\src\assets\wasm\`, replace if necessary. - In order for all features to work as expected, the node and it's runtime version should be chosen based on the current `@gear-js/api` version. In case of issues with the application, try to switch to another network or run your own local node and specify its address in the `.env` file. When applicable, make sure the smart contract(s) wasm files are uploaded and running in this network accordingly. diff --git a/frontend/apps/syndote/src/app/hooks/index.ts b/frontend/apps/syndote/src/app/hooks/index.ts new file mode 100644 index 000000000..155438768 --- /dev/null +++ b/frontend/apps/syndote/src/app/hooks/index.ts @@ -0,0 +1,3 @@ +export { usePending } from './use-pending'; +export * from './use-execute-with-pendig'; +export * from './use-sign-and-send'; diff --git a/frontend/apps/syndote/src/app/hooks/use-execute-with-pendig.ts b/frontend/apps/syndote/src/app/hooks/use-execute-with-pendig.ts new file mode 100644 index 000000000..6682a387f --- /dev/null +++ b/frontend/apps/syndote/src/app/hooks/use-execute-with-pendig.ts @@ -0,0 +1,35 @@ +import { useAlert } from '@gear-js/react-hooks'; +import { usePending } from './use-pending'; +import { getPanicType } from 'utils'; + +export type Options = { + onSuccess?: () => void; + onError?: (error?: unknown) => void; +}; + +export function useExecuteWithPending() { + const { setPending } = usePending(); + const alert = useAlert(); + + const executeWithPending = async (action: () => Promise, options?: Options) => { + try { + setPending(true); + await action(); + options?.onSuccess?.(); + } catch (error) { + console.error(error); + options?.onError?.(error); + + const panicType = getPanicType(error); + const alertError = typeof error === 'string' ? error : panicType; + + if (alertError) { + alert.error(alertError); + } + } finally { + setPending(false); + } + }; + + return { executeWithPending }; +} diff --git a/frontend/apps/syndote/src/app/hooks/use-pending.ts b/frontend/apps/syndote/src/app/hooks/use-pending.ts new file mode 100644 index 000000000..722df732d --- /dev/null +++ b/frontend/apps/syndote/src/app/hooks/use-pending.ts @@ -0,0 +1,8 @@ +import { IS_LOADING } from 'atoms'; +import { useAtom } from 'jotai'; + +export function usePending() { + const [pending, setPending] = useAtom(IS_LOADING); + + return { pending, setPending }; +} diff --git a/frontend/apps/syndote/src/app/hooks/use-sign-and-send.ts b/frontend/apps/syndote/src/app/hooks/use-sign-and-send.ts new file mode 100644 index 000000000..904a9bbda --- /dev/null +++ b/frontend/apps/syndote/src/app/hooks/use-sign-and-send.ts @@ -0,0 +1,28 @@ +import { useCheckBalance } from '@dapps-frontend/hooks'; +import { GenericTransactionReturn, TransactionReturn } from '@gear-js/react-hooks/dist/esm/hooks/sails/types'; + +export const useSignAndSend = () => { + const { checkBalance } = useCheckBalance(); + + const signAndSend = async (transaction: TransactionReturn<() => GenericTransactionReturn>) => { + const calculatedGas = Number(transaction.extrinsic.args[2].toString()); + + return new Promise((resolve, reject) => { + checkBalance( + calculatedGas, + async () => { + try { + const { response } = await transaction.signAndSend(); + await response(); + resolve(); + } catch (e) { + reject(e); + } + }, + () => reject(), + ); + }); + }; + + return { signAndSend }; +}; diff --git a/frontend/apps/syndote/src/app/utils/index.ts b/frontend/apps/syndote/src/app/utils/index.ts new file mode 100644 index 000000000..15858e186 --- /dev/null +++ b/frontend/apps/syndote/src/app/utils/index.ts @@ -0,0 +1 @@ +export * from './sails'; diff --git a/frontend/apps/syndote/src/app/utils/sails/events/index.ts b/frontend/apps/syndote/src/app/utils/sails/events/index.ts new file mode 100644 index 000000000..922157868 --- /dev/null +++ b/frontend/apps/syndote/src/app/utils/sails/events/index.ts @@ -0,0 +1,2 @@ +export { useEventStepSubscription } from './use-event-step-subscription'; +export { useEventGameCanceledSubscription } from './use-event-game-canceled-subscription'; diff --git a/frontend/apps/syndote/src/app/utils/sails/events/use-event-game-canceled-subscription.ts b/frontend/apps/syndote/src/app/utils/sails/events/use-event-game-canceled-subscription.ts new file mode 100644 index 000000000..3691cc398 --- /dev/null +++ b/frontend/apps/syndote/src/app/utils/sails/events/use-event-game-canceled-subscription.ts @@ -0,0 +1,20 @@ +import { useProgramEvent } from '@gear-js/react-hooks'; +import { useProgram } from 'app/utils'; + +export function useEventGameCanceledSubscription(onData: () => void) { + const program = useProgram(); + + useProgramEvent({ + program, + serviceName: 'syndote', + functionName: 'subscribeToGameWasCancelledEvent', + onData, + }); + + useProgramEvent({ + program, + serviceName: 'syndote', + functionName: 'subscribeToGameDeletedEvent', + onData, + }); +} diff --git a/frontend/apps/syndote/src/app/utils/sails/events/use-event-step-subscription.ts b/frontend/apps/syndote/src/app/utils/sails/events/use-event-step-subscription.ts new file mode 100644 index 000000000..92561e896 --- /dev/null +++ b/frontend/apps/syndote/src/app/utils/sails/events/use-event-step-subscription.ts @@ -0,0 +1,14 @@ +import { useProgramEvent } from '@gear-js/react-hooks'; +import { useProgram } from 'app/utils'; +import { Step } from 'types'; + +export function useEventStepSubscription(onData: (data: Step) => void) { + const program = useProgram(); + + useProgramEvent({ + program, + serviceName: 'syndote', + functionName: 'subscribeToStepEvent', + onData, + }); +} diff --git a/frontend/apps/syndote/src/app/utils/sails/index.ts b/frontend/apps/syndote/src/app/utils/sails/index.ts new file mode 100644 index 000000000..d2e928883 --- /dev/null +++ b/frontend/apps/syndote/src/app/utils/sails/index.ts @@ -0,0 +1,5 @@ +export * from './sails'; +export * from './syndote'; +export * from './events'; +export * from './queries'; +export * from './messages'; diff --git a/frontend/apps/syndote/src/app/utils/sails/messages/index.ts b/frontend/apps/syndote/src/app/utils/sails/messages/index.ts new file mode 100644 index 000000000..01cf8944d --- /dev/null +++ b/frontend/apps/syndote/src/app/utils/sails/messages/index.ts @@ -0,0 +1,8 @@ +export { useAddGasToPlayerStrategyMessage } from './use-add-gas-to-player-strategy-message'; +export { useCancelGameSessionMessage } from './use-cancel-game-session-message'; +export { useCreateGameSessionMessage } from './use-create-game-session-message'; +export { useDeleteGameMessage } from './use-delete-game-message'; +export { useDeletePlayerMessage } from './use-delete-player-message'; +export { useExitGameMessage } from './use-exit-game-message'; +export { usePlayMessage } from './use-play-message'; +export { useRegisterMessage } from './use-register-message'; diff --git a/frontend/apps/syndote/src/app/utils/sails/messages/use-add-gas-to-player-strategy-message.ts b/frontend/apps/syndote/src/app/utils/sails/messages/use-add-gas-to-player-strategy-message.ts new file mode 100644 index 000000000..e0e058204 --- /dev/null +++ b/frontend/apps/syndote/src/app/utils/sails/messages/use-add-gas-to-player-strategy-message.ts @@ -0,0 +1,30 @@ +import { HexString } from '@gear-js/api'; +import { usePrepareProgramTransaction } from '@gear-js/react-hooks'; +import { useProgram } from 'app/utils'; +import { Options, useExecuteWithPending, useSignAndSend } from 'app/hooks'; + +type Params = { + adminId: HexString; +}; + +export const useAddGasToPlayerStrategyMessage = () => { + const program = useProgram(); + const { prepareTransactionAsync } = usePrepareProgramTransaction({ + program, + serviceName: 'syndote', + functionName: 'addGasToPlayerStrategy', + }); + const { signAndSend } = useSignAndSend(); + const { executeWithPending } = useExecuteWithPending(); + + const addGasToPlayerStrategyMessage = async ({ adminId }: Params, options?: Options) => + executeWithPending(async () => { + const { transaction } = await prepareTransactionAsync({ + args: [adminId], + gasLimit: { increaseGas: 10 }, + }); + await signAndSend(transaction); + }, options); + + return { addGasToPlayerStrategyMessage }; +}; diff --git a/frontend/apps/syndote/src/app/utils/sails/messages/use-cancel-game-session-message.ts b/frontend/apps/syndote/src/app/utils/sails/messages/use-cancel-game-session-message.ts new file mode 100644 index 000000000..3545f0d7e --- /dev/null +++ b/frontend/apps/syndote/src/app/utils/sails/messages/use-cancel-game-session-message.ts @@ -0,0 +1,30 @@ +import { HexString } from '@gear-js/api'; +import { usePrepareProgramTransaction } from '@gear-js/react-hooks'; +import { useProgram } from 'app/utils'; +import { Options, useExecuteWithPending, useSignAndSend } from 'app/hooks'; + +type Params = { + adminId: HexString; +}; + +export const useCancelGameSessionMessage = () => { + const program = useProgram(); + const { prepareTransactionAsync } = usePrepareProgramTransaction({ + program, + serviceName: 'syndote', + functionName: 'cancelGameSession', + }); + const { signAndSend } = useSignAndSend(); + const { executeWithPending } = useExecuteWithPending(); + + const cancelGameSessionMessage = async ({ adminId }: Params, options?: Options) => + executeWithPending(async () => { + const { transaction } = await prepareTransactionAsync({ + args: [adminId], + gasLimit: { increaseGas: 10 }, + }); + await signAndSend(transaction); + }, options); + + return { cancelGameSessionMessage }; +}; diff --git a/frontend/apps/syndote/src/app/utils/sails/messages/use-create-game-session-message.ts b/frontend/apps/syndote/src/app/utils/sails/messages/use-create-game-session-message.ts new file mode 100644 index 000000000..b73d18abb --- /dev/null +++ b/frontend/apps/syndote/src/app/utils/sails/messages/use-create-game-session-message.ts @@ -0,0 +1,34 @@ +import { HexString } from '@gear-js/api'; +import { usePrepareProgramTransaction } from '@gear-js/react-hooks'; +import { useProgram } from 'app/utils'; +import { Options, useExecuteWithPending, useSignAndSend } from 'app/hooks'; + +type Params = { + value?: bigint; + entryFee: number | string | bigint | null; + name: string; + strategyId: HexString; +}; + +export const useCreateGameSessionMessage = () => { + const program = useProgram(); + const { prepareTransactionAsync } = usePrepareProgramTransaction({ + program, + serviceName: 'syndote', + functionName: 'createGameSession', + }); + const { signAndSend } = useSignAndSend(); + const { executeWithPending } = useExecuteWithPending(); + + const createGameSessionMessage = async ({ value, entryFee, name, strategyId }: Params, options?: Options) => + executeWithPending(async () => { + const { transaction } = await prepareTransactionAsync({ + args: [entryFee, name, strategyId], + gasLimit: { increaseGas: 10 }, + value, + }); + await signAndSend(transaction); + }, options); + + return { createGameSessionMessage }; +}; diff --git a/frontend/apps/syndote/src/app/utils/sails/messages/use-delete-game-message.ts b/frontend/apps/syndote/src/app/utils/sails/messages/use-delete-game-message.ts new file mode 100644 index 000000000..3ec4b7d3c --- /dev/null +++ b/frontend/apps/syndote/src/app/utils/sails/messages/use-delete-game-message.ts @@ -0,0 +1,30 @@ +import { HexString } from '@gear-js/api'; +import { usePrepareProgramTransaction } from '@gear-js/react-hooks'; +import { useProgram } from 'app/utils'; +import { Options, useExecuteWithPending, useSignAndSend } from 'app/hooks'; + +type Params = { + adminId: HexString; +}; + +export const useDeleteGameMessage = () => { + const program = useProgram(); + const { prepareTransactionAsync } = usePrepareProgramTransaction({ + program, + serviceName: 'syndote', + functionName: 'deleteGame', + }); + const { signAndSend } = useSignAndSend(); + const { executeWithPending } = useExecuteWithPending(); + + const deleteGameMessage = ({ adminId }: Params, options?: Options) => + executeWithPending(async () => { + const { transaction } = await prepareTransactionAsync({ + args: [adminId], + gasLimit: { increaseGas: 10 }, + }); + await signAndSend(transaction); + }, options); + + return { deleteGameMessage }; +}; diff --git a/frontend/apps/syndote/src/app/utils/sails/messages/use-delete-player-message.ts b/frontend/apps/syndote/src/app/utils/sails/messages/use-delete-player-message.ts new file mode 100644 index 000000000..2becc26a8 --- /dev/null +++ b/frontend/apps/syndote/src/app/utils/sails/messages/use-delete-player-message.ts @@ -0,0 +1,30 @@ +import { HexString } from '@gear-js/api'; +import { usePrepareProgramTransaction } from '@gear-js/react-hooks'; +import { useProgram } from 'app/utils'; +import { Options, useExecuteWithPending, useSignAndSend } from 'app/hooks'; + +type Params = { + playerId: HexString; +}; + +export const useDeletePlayerMessage = () => { + const program = useProgram(); + const { prepareTransactionAsync } = usePrepareProgramTransaction({ + program, + serviceName: 'syndote', + functionName: 'deletePlayer', + }); + const { signAndSend } = useSignAndSend(); + const { executeWithPending } = useExecuteWithPending(); + + const deletePlayerMessage = ({ playerId }: Params, options?: Options) => + executeWithPending(async () => { + const { transaction } = await prepareTransactionAsync({ + args: [playerId], + gasLimit: { increaseGas: 10 }, + }); + await signAndSend(transaction); + }, options); + + return { deletePlayerMessage }; +}; diff --git a/frontend/apps/syndote/src/app/utils/sails/messages/use-exit-game-message.ts b/frontend/apps/syndote/src/app/utils/sails/messages/use-exit-game-message.ts new file mode 100644 index 000000000..721ac8836 --- /dev/null +++ b/frontend/apps/syndote/src/app/utils/sails/messages/use-exit-game-message.ts @@ -0,0 +1,30 @@ +import { HexString } from '@gear-js/api'; +import { usePrepareProgramTransaction } from '@gear-js/react-hooks'; +import { useProgram } from 'app/utils'; +import { Options, useExecuteWithPending, useSignAndSend } from 'app/hooks'; + +type Params = { + adminId: HexString; +}; + +export const useExitGameMessage = () => { + const program = useProgram(); + const { prepareTransactionAsync } = usePrepareProgramTransaction({ + program, + serviceName: 'syndote', + functionName: 'exitGame', + }); + const { signAndSend } = useSignAndSend(); + const { executeWithPending } = useExecuteWithPending(); + + const exitGameMessage = ({ adminId }: Params, options?: Options) => + executeWithPending(async () => { + const { transaction } = await prepareTransactionAsync({ + args: [adminId], + gasLimit: { increaseGas: 10 }, + }); + await signAndSend(transaction); + }, options); + + return { exitGameMessage }; +}; diff --git a/frontend/apps/syndote/src/app/utils/sails/messages/use-play-message.ts b/frontend/apps/syndote/src/app/utils/sails/messages/use-play-message.ts new file mode 100644 index 000000000..bd32d3806 --- /dev/null +++ b/frontend/apps/syndote/src/app/utils/sails/messages/use-play-message.ts @@ -0,0 +1,30 @@ +import { HexString } from '@gear-js/api'; +import { usePrepareProgramTransaction } from '@gear-js/react-hooks'; +import { useProgram } from 'app/utils'; +import { Options, useExecuteWithPending, useSignAndSend } from 'app/hooks'; + +type Params = { + adminId: HexString; +}; + +export const usePlayMessage = () => { + const program = useProgram(); + const { prepareTransactionAsync } = usePrepareProgramTransaction({ + program, + serviceName: 'syndote', + functionName: 'play', + }); + const { signAndSend } = useSignAndSend(); + const { executeWithPending } = useExecuteWithPending(); + + const playMessage = async ({ adminId }: Params, options?: Options) => + executeWithPending(async () => { + const { transaction } = await prepareTransactionAsync({ + args: [adminId], + gasLimit: BigInt(730000000000), + }); + await signAndSend(transaction); + }, options); + + return { playMessage }; +}; diff --git a/frontend/apps/syndote/src/app/utils/sails/messages/use-register-message.ts b/frontend/apps/syndote/src/app/utils/sails/messages/use-register-message.ts new file mode 100644 index 000000000..61cfbf61a --- /dev/null +++ b/frontend/apps/syndote/src/app/utils/sails/messages/use-register-message.ts @@ -0,0 +1,34 @@ +import { usePrepareProgramTransaction } from '@gear-js/react-hooks'; +import { useProgram } from 'app/utils'; +import { Options, useExecuteWithPending, useSignAndSend } from 'app/hooks'; +import { HexString } from '@gear-js/api'; + +type Params = { + value?: bigint; + adminId: HexString; + strategyId: HexString; + name: string; +}; + +export const useRegisterMessage = () => { + const program = useProgram(); + const { prepareTransactionAsync } = usePrepareProgramTransaction({ + program, + serviceName: 'syndote', + functionName: 'register', + }); + const { signAndSend } = useSignAndSend(); + const { executeWithPending } = useExecuteWithPending(); + + const registerMessage = async ({ value, adminId, strategyId, name }: Params, options?: Options) => + executeWithPending(async () => { + const { transaction } = await prepareTransactionAsync({ + args: [adminId, strategyId, name], + gasLimit: { increaseGas: 10 }, + value, + }); + await signAndSend(transaction); + }, options); + + return { registerMessage }; +}; diff --git a/frontend/apps/syndote/src/app/utils/sails/queries/index.ts b/frontend/apps/syndote/src/app/utils/sails/queries/index.ts new file mode 100644 index 000000000..863068017 --- /dev/null +++ b/frontend/apps/syndote/src/app/utils/sails/queries/index.ts @@ -0,0 +1 @@ +export { useGetGameSessionQuery } from './use-get-game-session-query'; diff --git a/frontend/apps/syndote/src/app/utils/sails/queries/use-get-game-session-query.ts b/frontend/apps/syndote/src/app/utils/sails/queries/use-get-game-session-query.ts new file mode 100644 index 000000000..b5b7ffc21 --- /dev/null +++ b/frontend/apps/syndote/src/app/utils/sails/queries/use-get-game-session-query.ts @@ -0,0 +1,24 @@ +import { useProgram } from 'app/utils'; +import { useAccount, useProgramQuery } from '@gear-js/react-hooks'; +import { HexString } from '@gear-js/api'; +import { useAtomValue } from 'jotai'; +import { CURRENT_GAME_ADMIN_ATOM } from 'atoms'; + +export const useGetGameSessionQuery = (address?: HexString | null, disabled?: boolean) => { + const program = useProgram(); + const { account } = useAccount(); + const admin = useAtomValue(CURRENT_GAME_ADMIN_ATOM); + + const gameAddress = address || admin || account?.decodedAddress; + + const { data, refetch, isFetching, isFetched, error } = useProgramQuery({ + program, + serviceName: 'syndote', + functionName: 'getGameSession', + args: [gameAddress || '0x'], + query: { enabled: !disabled && gameAddress ? undefined : false }, + watch: true, + }); + + return { state: data, isFetching, isFetched, refetch, error }; +}; diff --git a/frontend/apps/syndote/src/app/utils/sails/sails.ts b/frontend/apps/syndote/src/app/utils/sails/sails.ts new file mode 100644 index 000000000..46a5eba4e --- /dev/null +++ b/frontend/apps/syndote/src/app/utils/sails/sails.ts @@ -0,0 +1,16 @@ +import { useProgram as useGearJsProgram } from '@gear-js/react-hooks'; +import { Program } from './syndote'; +import { useDnsProgramIds } from '@dapps-frontend/hooks'; + +const useProgram = () => { + const { programId } = useDnsProgramIds(); + + const { data: program } = useGearJsProgram({ + library: Program, + id: programId, + }); + + return program; +}; + +export { useProgram }; diff --git a/frontend/apps/syndote/src/app/utils/sails/syndote.idl b/frontend/apps/syndote/src/app/utils/sails/syndote.idl new file mode 100644 index 000000000..d2b64452a --- /dev/null +++ b/frontend/apps/syndote/src/app/utils/sails/syndote.idl @@ -0,0 +1,100 @@ +type Config = struct { + reservation_amount: u64, + reservation_duration_in_block: u32, + time_for_step: u32, + min_gas_limit: u64, + gas_refill_timeout: u32, + gas_for_step: u64, +}; + +type GameState = struct { + admin_id: actor_id, + properties_in_bank: vec u8, + round: u128, + players: vec struct { actor_id, PlayerInfoState }, + owners_to_strategy_ids: vec struct { actor_id, actor_id }, + players_queue: vec actor_id, + current_turn: u8, + current_player: actor_id, + current_step: u64, + properties: vec opt struct { actor_id, vec Gear, u32, u32 }, + ownership: vec actor_id, + game_status: GameStatus, + winner: actor_id, + reservations: vec ReservationId, + entry_fee: opt u128, + prize_pool: u128, +}; + +type PlayerInfoState = struct { + owner_id: actor_id, + name: str, + position: u8, + balance: u32, + debt: u32, + in_jail: bool, + round: u128, + cells: vec u8, + penalty: u8, + lost: bool, + reservation_id: opt ReservationId, +}; + +type ReservationId = struct { + [u8, 32], +}; + +type Gear = enum { + Bronze, + Silver, + Gold, +}; + +type GameStatus = enum { + Registration, + Play, + Finished, + Wait, + WaitingForGasForGameContract, + WaitingForGasForStrategy: actor_id, +}; + +constructor { + New : (config: Config, dns_id_and_name: opt struct { actor_id, str }); +}; + +service Syndote { + AddGasToPlayerStrategy : (admin_id: actor_id) -> null; + CancelGameSession : (admin_id: actor_id) -> null; + ChangeAdmin : (admin: actor_id) -> null; + CreateGameSession : (entry_fee: opt u128, name: str, strategy_id: actor_id) -> null; + DeleteGame : (admin_id: actor_id) -> null; + DeletePlayer : (player_id: actor_id) -> null; + ExitGame : (admin_id: actor_id) -> null; + Kill : (inheritor: actor_id) -> null; + MakeReservation : (admin_id: actor_id) -> null; + Play : (admin_id: actor_id) -> null; + Register : (admin_id: actor_id, strategy_id: actor_id, name: str) -> null; + query GetConfig : () -> Config; + query GetGameSession : (account_id: actor_id) -> opt GameState; + query GetOwnerId : (admin_id: actor_id, strategy_id: actor_id) -> opt actor_id; + query GetPlayerInfo : (account_id: actor_id) -> opt PlayerInfoState; + query GetPlayersToSessions : () -> vec struct { actor_id, actor_id }; + + events { + GameSessionCreated: struct { admin_id: actor_id }; + ReservationMade; + StrategyRegistered; + GameFinished: struct { admin_id: actor_id, winner: actor_id }; + GasForPlayerStrategyAdded; + GameWasCancelled; + PlayerLeftGame; + Step: struct { players: vec struct { actor_id, PlayerInfoState }, properties: vec opt struct { actor_id, vec Gear, u32, u32 }, current_player: actor_id, ownership: vec actor_id, current_step: u64 }; + NextRoundFromReservation; + GameDeleted; + PlayerDeleted; + StrategicSuccess; + Killed: struct { inheritor: actor_id }; + } +}; + diff --git a/frontend/apps/syndote/src/app/utils/sails/syndote.ts b/frontend/apps/syndote/src/app/utils/sails/syndote.ts new file mode 100644 index 000000000..3d5e7ceab --- /dev/null +++ b/frontend/apps/syndote/src/app/utils/sails/syndote.ts @@ -0,0 +1,624 @@ +import { TransactionBuilder, getServiceNamePrefix, getFnNamePrefix, ZERO_ADDRESS } from 'sails-js'; +import { GearApi, HexString, decodeAddress } from '@gear-js/api'; +import { TypeRegistry } from '@polkadot/types'; + +type ActorId = HexString; + +export interface Config { + reservation_amount: number | string | bigint; + reservation_duration_in_block: number; + time_for_step: number; + min_gas_limit: number | string | bigint; + gas_refill_timeout: number; + gas_for_step: number | string | bigint; +} + +export interface GameState { + admin_id: ActorId; + properties_in_bank: `0x${string}`; + round: number | string | bigint; + players: Array<[ActorId, PlayerInfoState]>; + owners_to_strategy_ids: Array<[ActorId, ActorId]>; + players_queue: Array; + current_turn: number; + current_player: ActorId; + current_step: number | string | bigint; + properties: Array<[ActorId, Array, number, number] | null>; + ownership: Array; + game_status: GameStatus; + winner: ActorId; + reservations: Array; + entry_fee: number | string | bigint | null; + prize_pool: number | string | bigint; +} + +export interface PlayerInfoState { + owner_id: ActorId; + name: string; + position: number; + balance: number; + debt: number; + in_jail: boolean; + round: number | string | bigint; + cells: `0x${string}`; + penalty: number; + lost: boolean; + reservation_id: ReservationId | null; +} + +export type ReservationId = [Array]; + +export type Gear = 'Bronze' | 'Silver' | 'Gold'; + +export type GameStatus = + | { registration: null } + | { play: null } + | { finished: null } + | { wait: null } + | { waitingForGasForGameContract: null } + | { waitingForGasForStrategy: ActorId }; + +export class Program { + public readonly registry: TypeRegistry; + public readonly syndote: Syndote; + + constructor(public api: GearApi, public programId?: `0x${string}`) { + const types: Record = { + Config: { + reservation_amount: 'u64', + reservation_duration_in_block: 'u32', + time_for_step: 'u32', + min_gas_limit: 'u64', + gas_refill_timeout: 'u32', + gas_for_step: 'u64', + }, + GameState: { + admin_id: '[u8;32]', + properties_in_bank: 'Vec', + round: 'u128', + players: 'Vec<([u8;32], PlayerInfoState)>', + owners_to_strategy_ids: 'Vec<([u8;32], [u8;32])>', + players_queue: 'Vec<[u8;32]>', + current_turn: 'u8', + current_player: '[u8;32]', + current_step: 'u64', + properties: 'Vec, u32, u32)>>', + ownership: 'Vec<[u8;32]>', + game_status: 'GameStatus', + winner: '[u8;32]', + reservations: 'Vec', + entry_fee: 'Option', + prize_pool: 'u128', + }, + PlayerInfoState: { + owner_id: '[u8;32]', + name: 'String', + position: 'u8', + balance: 'u32', + debt: 'u32', + in_jail: 'bool', + round: 'u128', + cells: 'Vec', + penalty: 'u8', + lost: 'bool', + reservation_id: 'Option', + }, + ReservationId: '([u8; 32])', + Gear: { _enum: ['Bronze', 'Silver', 'Gold'] }, + GameStatus: { + _enum: { + Registration: 'Null', + Play: 'Null', + Finished: 'Null', + Wait: 'Null', + WaitingForGasForGameContract: 'Null', + WaitingForGasForStrategy: '[u8;32]', + }, + }, + }; + + this.registry = new TypeRegistry(); + this.registry.setKnownTypes({ types }); + this.registry.register(types); + + this.syndote = new Syndote(this); + } + + newCtorFromCode( + code: Uint8Array | Buffer, + config: Config, + dns_id_and_name: [ActorId, string] | null, + ): TransactionBuilder { + const builder = new TransactionBuilder( + this.api, + this.registry, + 'upload_program', + ['New', config, dns_id_and_name], + '(String, Config, Option<([u8;32], String)>)', + 'String', + code, + ); + + this.programId = builder.programId; + return builder; + } + + newCtorFromCodeId(codeId: `0x${string}`, config: Config, dns_id_and_name: [ActorId, string] | null) { + const builder = new TransactionBuilder( + this.api, + this.registry, + 'create_program', + ['New', config, dns_id_and_name], + '(String, Config, Option<([u8;32], String)>)', + 'String', + codeId, + ); + + this.programId = builder.programId; + return builder; + } +} + +export class Syndote { + constructor(private _program: Program) {} + + public addGasToPlayerStrategy(admin_id: ActorId): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Syndote', 'AddGasToPlayerStrategy', admin_id], + '(String, String, [u8;32])', + 'Null', + this._program.programId, + ); + } + + public cancelGameSession(admin_id: ActorId): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Syndote', 'CancelGameSession', admin_id], + '(String, String, [u8;32])', + 'Null', + this._program.programId, + ); + } + + public changeAdmin(admin: ActorId): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Syndote', 'ChangeAdmin', admin], + '(String, String, [u8;32])', + 'Null', + this._program.programId, + ); + } + + public createGameSession( + entry_fee: number | string | bigint | null, + name: string, + strategy_id: ActorId, + ): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Syndote', 'CreateGameSession', entry_fee, name, strategy_id], + '(String, String, Option, String, [u8;32])', + 'Null', + this._program.programId, + ); + } + + public deleteGame(admin_id: ActorId): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Syndote', 'DeleteGame', admin_id], + '(String, String, [u8;32])', + 'Null', + this._program.programId, + ); + } + + public deletePlayer(player_id: ActorId): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Syndote', 'DeletePlayer', player_id], + '(String, String, [u8;32])', + 'Null', + this._program.programId, + ); + } + + public exitGame(admin_id: ActorId): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Syndote', 'ExitGame', admin_id], + '(String, String, [u8;32])', + 'Null', + this._program.programId, + ); + } + + public kill(inheritor: ActorId): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Syndote', 'Kill', inheritor], + '(String, String, [u8;32])', + 'Null', + this._program.programId, + ); + } + + public makeReservation(admin_id: ActorId): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Syndote', 'MakeReservation', admin_id], + '(String, String, [u8;32])', + 'Null', + this._program.programId, + ); + } + + public play(admin_id: ActorId): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Syndote', 'Play', admin_id], + '(String, String, [u8;32])', + 'Null', + this._program.programId, + ); + } + + public register(admin_id: ActorId, strategy_id: ActorId, name: string): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Syndote', 'Register', admin_id, strategy_id, name], + '(String, String, [u8;32], [u8;32], String)', + 'Null', + this._program.programId, + ); + } + + public async getConfig( + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise { + const payload = this._program.registry.createType('(String, String)', ['Syndote', 'GetConfig']).toHex(); + const reply = await this._program.api.message.calculateReply({ + destination: this._program.programId!, + origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS, + payload, + value: value || 0, + gasLimit: this._program.api.blockGasLimit.toBigInt(), + at: atBlock, + }); + if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString()); + const result = this._program.registry.createType('(String, String, Config)', reply.payload); + return result[2].toJSON() as unknown as Config; + } + + public async getGameSession( + account_id: ActorId, + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise { + const payload = this._program.registry + .createType('(String, String, [u8;32])', ['Syndote', 'GetGameSession', account_id]) + .toHex(); + const reply = await this._program.api.message.calculateReply({ + destination: this._program.programId!, + origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS, + payload, + value: value || 0, + gasLimit: this._program.api.blockGasLimit.toBigInt(), + at: atBlock, + }); + if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString()); + const result = this._program.registry.createType('(String, String, Option)', reply.payload); + return result[2].toJSON() as unknown as GameState | null; + } + + public async getOwnerId( + admin_id: ActorId, + strategy_id: ActorId, + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise { + const payload = this._program.registry + .createType('(String, String, [u8;32], [u8;32])', ['Syndote', 'GetOwnerId', admin_id, strategy_id]) + .toHex(); + const reply = await this._program.api.message.calculateReply({ + destination: this._program.programId!, + origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS, + payload, + value: value || 0, + gasLimit: this._program.api.blockGasLimit.toBigInt(), + at: atBlock, + }); + if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString()); + const result = this._program.registry.createType('(String, String, Option<[u8;32]>)', reply.payload); + return result[2].toJSON() as unknown as ActorId | null; + } + + public async getPlayerInfo( + account_id: ActorId, + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise { + const payload = this._program.registry + .createType('(String, String, [u8;32])', ['Syndote', 'GetPlayerInfo', account_id]) + .toHex(); + const reply = await this._program.api.message.calculateReply({ + destination: this._program.programId!, + origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS, + payload, + value: value || 0, + gasLimit: this._program.api.blockGasLimit.toBigInt(), + at: atBlock, + }); + if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString()); + const result = this._program.registry.createType('(String, String, Option)', reply.payload); + return result[2].toJSON() as unknown as PlayerInfoState | null; + } + + public async getPlayersToSessions( + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise> { + const payload = this._program.registry.createType('(String, String)', ['Syndote', 'GetPlayersToSessions']).toHex(); + const reply = await this._program.api.message.calculateReply({ + destination: this._program.programId!, + origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS, + payload, + value: value || 0, + gasLimit: this._program.api.blockGasLimit.toBigInt(), + at: atBlock, + }); + if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString()); + const result = this._program.registry.createType('(String, String, Vec<([u8;32], [u8;32])>)', reply.payload); + return result[2].toJSON() as unknown as Array<[ActorId, ActorId]>; + } + + public subscribeToGameSessionCreatedEvent( + callback: (data: { admin_id: ActorId }) => void | Promise, + ): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'Syndote' && getFnNamePrefix(payload) === 'GameSessionCreated') { + callback( + this._program.registry + .createType('(String, String, {"admin_id":"[u8;32]"})', message.payload)[2] + .toJSON() as unknown as { admin_id: ActorId }, + ); + } + }); + } + + public subscribeToReservationMadeEvent(callback: (data: null) => void | Promise): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'Syndote' && getFnNamePrefix(payload) === 'ReservationMade') { + callback(null); + } + }); + } + + public subscribeToStrategyRegisteredEvent(callback: (data: null) => void | Promise): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'Syndote' && getFnNamePrefix(payload) === 'StrategyRegistered') { + callback(null); + } + }); + } + + public subscribeToGameFinishedEvent( + callback: (data: { admin_id: ActorId; winner: ActorId }) => void | Promise, + ): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'Syndote' && getFnNamePrefix(payload) === 'GameFinished') { + callback( + this._program.registry + .createType('(String, String, {"admin_id":"[u8;32]","winner":"[u8;32]"})', message.payload)[2] + .toJSON() as unknown as { admin_id: ActorId; winner: ActorId }, + ); + } + }); + } + + public subscribeToGasForPlayerStrategyAddedEvent( + callback: (data: null) => void | Promise, + ): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'Syndote' && getFnNamePrefix(payload) === 'GasForPlayerStrategyAdded') { + callback(null); + } + }); + } + + public subscribeToGameWasCancelledEvent(callback: (data: null) => void | Promise): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'Syndote' && getFnNamePrefix(payload) === 'GameWasCancelled') { + callback(null); + } + }); + } + + public subscribeToPlayerLeftGameEvent(callback: (data: null) => void | Promise): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'Syndote' && getFnNamePrefix(payload) === 'PlayerLeftGame') { + callback(null); + } + }); + } + + public subscribeToStepEvent( + callback: (data: { + players: Array<[ActorId, PlayerInfoState]>; + properties: Array<[ActorId, Array, number, number] | null>; + current_player: ActorId; + ownership: Array; + current_step: number | string | bigint; + }) => void | Promise, + ): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'Syndote' && getFnNamePrefix(payload) === 'Step') { + callback( + this._program.registry + .createType( + '(String, String, {"players":"Vec<([u8;32], PlayerInfoState)>","properties":"Vec, u32, u32)>>","current_player":"[u8;32]","ownership":"Vec<[u8;32]>","current_step":"u64"})', + message.payload, + )[2] + .toJSON() as unknown as { + players: Array<[ActorId, PlayerInfoState]>; + properties: Array<[ActorId, Array, number, number] | null>; + current_player: ActorId; + ownership: Array; + current_step: number | string | bigint; + }, + ); + } + }); + } + + public subscribeToNextRoundFromReservationEvent(callback: (data: null) => void | Promise): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'Syndote' && getFnNamePrefix(payload) === 'NextRoundFromReservation') { + callback(null); + } + }); + } + + public subscribeToGameDeletedEvent(callback: (data: null) => void | Promise): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'Syndote' && getFnNamePrefix(payload) === 'GameDeleted') { + callback(null); + } + }); + } + + public subscribeToPlayerDeletedEvent(callback: (data: null) => void | Promise): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'Syndote' && getFnNamePrefix(payload) === 'PlayerDeleted') { + callback(null); + } + }); + } + + public subscribeToStrategicSuccessEvent(callback: (data: null) => void | Promise): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'Syndote' && getFnNamePrefix(payload) === 'StrategicSuccess') { + callback(null); + } + }); + } + + public subscribeToKilledEvent(callback: (data: { inheritor: ActorId }) => void | Promise): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'Syndote' && getFnNamePrefix(payload) === 'Killed') { + callback( + this._program.registry + .createType('(String, String, {"inheritor":"[u8;32]"})', message.payload)[2] + .toJSON() as unknown as { inheritor: ActorId }, + ); + } + }); + } +} diff --git a/frontend/apps/syndote/src/app/utils/sails/syndote_player.idl b/frontend/apps/syndote/src/app/utils/sails/syndote_player.idl new file mode 100644 index 000000000..35df17eee --- /dev/null +++ b/frontend/apps/syndote/src/app/utils/sails/syndote_player.idl @@ -0,0 +1,25 @@ +type PlayerInfo = struct { + position: u8, + balance: u32, + debt: u32, + in_jail: bool, + round: u128, + cells: vec u8, + penalty: u8, + lost: bool, +}; + +type Gear = enum { + Bronze, + Silver, + Gold, +}; + +constructor { + New : (); +}; + +service Player { + query YourTurn : (players: vec struct { actor_id, PlayerInfo }, properties: vec opt struct { actor_id, vec Gear, u32, u32 }) -> bool; +}; + diff --git a/frontend/apps/syndote/src/app/utils/sails/syndote_player.ts b/frontend/apps/syndote/src/app/utils/sails/syndote_player.ts new file mode 100644 index 000000000..1d6b4dd05 --- /dev/null +++ b/frontend/apps/syndote/src/app/utils/sails/syndote_player.ts @@ -0,0 +1,105 @@ +import { GearApi, decodeAddress } from '@gear-js/api'; +import { TypeRegistry } from '@polkadot/types'; +import { TransactionBuilder, ActorId, ZERO_ADDRESS } from 'sails-js'; + +export interface PlayerInfo { + position: number; + balance: number; + debt: number; + in_jail: boolean; + round: number | string | bigint; + cells: `0x${string}`; + penalty: number; + lost: boolean; +} + +export type Gear = 'bronze' | 'silver' | 'gold'; + +export class Program { + public readonly registry: TypeRegistry; + public readonly player: Player; + + constructor(public api: GearApi, public programId?: `0x${string}`) { + const types: Record = { + PlayerInfo: { + position: 'u8', + balance: 'u32', + debt: 'u32', + in_jail: 'bool', + round: 'u128', + cells: 'Vec', + penalty: 'u8', + lost: 'bool', + }, + Gear: { _enum: ['Bronze', 'Silver', 'Gold'] }, + }; + + this.registry = new TypeRegistry(); + this.registry.setKnownTypes({ types }); + this.registry.register(types); + + this.player = new Player(this); + } + + newCtorFromCode(code: Uint8Array | Buffer): TransactionBuilder { + const builder = new TransactionBuilder( + this.api, + this.registry, + 'upload_program', + 'New', + 'String', + 'String', + code, + ); + + this.programId = builder.programId; + return builder; + } + + newCtorFromCodeId(codeId: `0x${string}`) { + const builder = new TransactionBuilder( + this.api, + this.registry, + 'create_program', + 'New', + 'String', + 'String', + codeId, + ); + + this.programId = builder.programId; + return builder; + } +} + +export class Player { + constructor(private _program: Program) {} + + public async yourTurn( + players: Array<[ActorId, PlayerInfo]>, + properties: Array<[ActorId, Array, number, number] | null>, + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise { + const payload = this._program.registry + .createType('(String, String, Vec<([u8;32], PlayerInfo)>, Vec, u32, u32)>>)', [ + 'Player', + 'YourTurn', + players, + properties, + ]) + .toHex(); + const reply = await this._program.api.message.calculateReply({ + destination: this._program.programId!, + origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS, + payload, + value: value || 0, + gasLimit: this._program.api.blockGasLimit.toBigInt(), + at: atBlock, + }); + if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString()); + const result = this._program.registry.createType('(String, String, bool)', reply.payload); + return result[2].toJSON() as unknown as boolean; + } +} diff --git a/frontend/apps/syndote/src/atoms.ts b/frontend/apps/syndote/src/atoms.ts index 7ba11de48..9d4115b0a 100644 --- a/frontend/apps/syndote/src/atoms.ts +++ b/frontend/apps/syndote/src/atoms.ts @@ -1,7 +1,8 @@ +import { HexString } from '@gear-js/api'; import { atom } from 'jotai'; // import { RegistrationStatus } from 'features/session/types'; -export const CURRENT_GAME_ADMIN_ATOM = atom(''); +export const CURRENT_GAME_ADMIN_ATOM = atom(null); export const CURRENT_STRATEGY_ID_ATOM = atom(''); diff --git a/frontend/apps/syndote/src/components/layout/header/Header.tsx b/frontend/apps/syndote/src/components/layout/header/Header.tsx index 7c6ca8e41..27aa0deb2 100644 --- a/frontend/apps/syndote/src/components/layout/header/Header.tsx +++ b/frontend/apps/syndote/src/components/layout/header/Header.tsx @@ -3,18 +3,18 @@ import { Header as CommonHeader, MenuHandler } from '@dapps-frontend/ui'; import { ReactComponent as VaraSVG } from 'assets/images/icons/logo-vara.svg'; import { ReactComponent as CrossSVG } from 'assets/images/icons/cross-icon.svg'; import { Button } from '@gear-js/vara-ui'; -import { useReadGameSessionState, useSyndoteMessage } from 'hooks/metadata'; import { useAccount } from '@gear-js/react-hooks'; -import clsx from 'clsx'; import { useQuitGame } from 'hooks/useQuitGame'; +import { useGetGameSessionQuery } from 'app/utils'; import styles from './Header.module.scss'; function Header() { - const { state } = useReadGameSessionState(); const { account } = useAccount(); - const { adminId, gameStatus } = state || {}; - const isAdmin = account?.decodedAddress === adminId; + const { state } = useGetGameSessionQuery(); + const { admin_id, game_status } = state || {}; + const isAdmin = account?.decodedAddress === admin_id; const { cancelGame, deleteGame, exitGame } = useQuitGame(); + const isFinished = game_status && 'finished' in game_status; return ( - {account?.decodedAddress && adminId === account?.decodedAddress && gameStatus !== 'Finished' && ( + {account?.decodedAddress && admin_id === account?.decodedAddress && isFinished && (