From f80c9ca301912fbfbfb2e0a4d3c8f7aa2506d522 Mon Sep 17 00:00:00 2001 From: Vadim Tokar <52737608+vraja-nayaka@users.noreply.github.com> Date: Fri, 13 Sep 2024 18:57:32 +0400 Subject: [PATCH] Racing-car-game: migrate to sails.js (#455) --- frontend/apps/racing-car-game/src/App.tsx | 17 +- .../racing-car-game/src/app/utils/index.ts | 1 + .../src/app/utils/sails/index.ts | 2 + .../src/app/utils/sails/lib.ts | 526 ++++++++++++++++++ .../src/app/utils/sails/sails.ts | 16 + .../racing-car-game/src/assets/meta/meta.txt | 1 - frontend/apps/racing-car-game/src/atoms.ts | 10 - frontend/apps/racing-car-game/src/consts.ts | 2 +- .../src/features/Game/atoms.ts | 11 - .../component/Heading/Heading.interface.ts | 4 +- .../Game/component/Layout/Layout.interface.ts | 23 - .../features/Game/component/Layout/Layout.tsx | 360 ++---------- .../Game/component/Road/Road.interface.ts | 3 +- .../src/features/Game/component/Road/Road.tsx | 14 +- .../src/features/Game/hooks.ts | 161 ------ .../src/features/Game/sails/events/index.ts | 1 + .../use-event-round-info-subscription.ts | 13 + .../src/features/Game/sails/index.ts | 3 + .../src/features/Game/sails/messages/index.ts | 2 + .../sails/messages/use-player-move-message.ts | 33 ++ .../sails/messages/use-start-game-message.ts | 33 ++ .../src/features/Game/sails/queries/index.ts | 1 + .../Game/sails/queries/use-game-query.ts | 17 + .../Main/components/Layout/Layout.tsx | 17 +- .../apps/racing-car-game/src/hocs/index.tsx | 12 +- .../src/hocs/query-provider.tsx | 23 + frontend/apps/racing-car-game/src/hooks.ts | 117 +--- frontend/apps/racing-car-game/src/types.ts | 80 +-- .../src/ui/Dropdown/Dropdown.interfaces.ts | 21 - .../src/ui/Dropdown/Dropdown.module.scss | 88 --- .../src/ui/Dropdown/Dropdown.tsx | 74 --- .../racing-car-game/src/ui/Dropdown/index.ts | 1 - frontend/apps/racing-car-game/src/ui/index.ts | 1 - frontend/apps/racing-car-game/src/utils.ts | 61 -- .../dev/gstd-racing-car-game/.env.example | 10 + .../dev/gstd-racing-car-game/.eslintrc.js | 46 ++ frontend/dev/gstd-racing-car-game/Dockerfile | 38 ++ frontend/dev/gstd-racing-car-game/README.md | 35 ++ .../gstd-racing-car-game/config-overrides.js | 26 + frontend/dev/gstd-racing-car-game/index.d.ts | 7 + 40 files changed, 898 insertions(+), 1013 deletions(-) create mode 100644 frontend/apps/racing-car-game/src/app/utils/index.ts create mode 100644 frontend/apps/racing-car-game/src/app/utils/sails/index.ts create mode 100644 frontend/apps/racing-car-game/src/app/utils/sails/lib.ts create mode 100644 frontend/apps/racing-car-game/src/app/utils/sails/sails.ts delete mode 100644 frontend/apps/racing-car-game/src/assets/meta/meta.txt delete mode 100644 frontend/apps/racing-car-game/src/atoms.ts delete mode 100644 frontend/apps/racing-car-game/src/features/Game/atoms.ts delete mode 100644 frontend/apps/racing-car-game/src/features/Game/component/Layout/Layout.interface.ts delete mode 100644 frontend/apps/racing-car-game/src/features/Game/hooks.ts create mode 100644 frontend/apps/racing-car-game/src/features/Game/sails/events/index.ts create mode 100644 frontend/apps/racing-car-game/src/features/Game/sails/events/use-event-round-info-subscription.ts create mode 100644 frontend/apps/racing-car-game/src/features/Game/sails/index.ts create mode 100644 frontend/apps/racing-car-game/src/features/Game/sails/messages/index.ts create mode 100644 frontend/apps/racing-car-game/src/features/Game/sails/messages/use-player-move-message.ts create mode 100644 frontend/apps/racing-car-game/src/features/Game/sails/messages/use-start-game-message.ts create mode 100644 frontend/apps/racing-car-game/src/features/Game/sails/queries/index.ts create mode 100644 frontend/apps/racing-car-game/src/features/Game/sails/queries/use-game-query.ts create mode 100644 frontend/apps/racing-car-game/src/hocs/query-provider.tsx delete mode 100644 frontend/apps/racing-car-game/src/ui/Dropdown/Dropdown.interfaces.ts delete mode 100644 frontend/apps/racing-car-game/src/ui/Dropdown/Dropdown.module.scss delete mode 100644 frontend/apps/racing-car-game/src/ui/Dropdown/Dropdown.tsx delete mode 100644 frontend/apps/racing-car-game/src/ui/Dropdown/index.ts create mode 100644 frontend/dev/gstd-racing-car-game/.env.example create mode 100644 frontend/dev/gstd-racing-car-game/.eslintrc.js create mode 100644 frontend/dev/gstd-racing-car-game/Dockerfile create mode 100644 frontend/dev/gstd-racing-car-game/README.md create mode 100644 frontend/dev/gstd-racing-car-game/config-overrides.js create mode 100644 frontend/dev/gstd-racing-car-game/index.d.ts diff --git a/frontend/apps/racing-car-game/src/App.tsx b/frontend/apps/racing-car-game/src/App.tsx index 3514b66d8..ea22064c8 100644 --- a/frontend/apps/racing-car-game/src/App.tsx +++ b/frontend/apps/racing-car-game/src/App.tsx @@ -1,6 +1,4 @@ -import { useEffect } from 'react'; import { Route, Routes } from 'react-router-dom'; -import { useSetAtom } from 'jotai'; import { useAccount, useApi } from '@gear-js/react-hooks'; import { ErrorTrackingRoutes } from '@dapps-frontend/error-tracking'; import { Container, Footer } from '@dapps-frontend/ui'; @@ -12,31 +10,18 @@ import { LOGIN, PLAY, START } from '@/App.routes'; import styles from './App.module.scss'; import 'babel-polyfill'; import { useLoginByParams } from './hooks'; -import { CURRENT_GAME, IS_CURRENT_GAME_READ_ATOM } from './atoms'; import { ProtectedRoute } from './features/Auth/components'; import { useAccountAvailableBalance, useAccountAvailableBalanceSync } from './features/Wallet/hooks'; import { LoginPage } from './pages/LoginPage'; import { ApiLoader } from './components/ApiLoader'; -import { useGameState } from './features/Game/hooks'; import { useAuth, useAuthSync } from './features/Auth/hooks'; import '@gear-js/vara-ui/dist/style.css'; function AppComponent() { const { isApiReady } = useApi(); - const { isAccountReady, account } = useAccount(); - const { state: game, isStateRead } = useGameState(); + const { isAccountReady } = useAccount(); const { isAvailableBalanceReady } = useAccountAvailableBalance(); const { isAuthReady } = useAuth(); - const setCurrentGame = useSetAtom(CURRENT_GAME); - const setIsCurrentRead = useSetAtom(IS_CURRENT_GAME_READ_ATOM); - - useEffect(() => { - if (isAccountReady && account?.decodedAddress && isStateRead) { - setCurrentGame(game.Game); - setIsCurrentRead(true); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [account?.decodedAddress, isAccountReady, isStateRead]); const isAppReady = isApiReady && isAccountReady && isAvailableBalanceReady && isAuthReady; diff --git a/frontend/apps/racing-car-game/src/app/utils/index.ts b/frontend/apps/racing-car-game/src/app/utils/index.ts new file mode 100644 index 000000000..15858e186 --- /dev/null +++ b/frontend/apps/racing-car-game/src/app/utils/index.ts @@ -0,0 +1 @@ +export * from './sails'; diff --git a/frontend/apps/racing-car-game/src/app/utils/sails/index.ts b/frontend/apps/racing-car-game/src/app/utils/sails/index.ts new file mode 100644 index 000000000..538de8615 --- /dev/null +++ b/frontend/apps/racing-car-game/src/app/utils/sails/index.ts @@ -0,0 +1,2 @@ +export * from './sails'; +export * from './lib'; diff --git a/frontend/apps/racing-car-game/src/app/utils/sails/lib.ts b/frontend/apps/racing-car-game/src/app/utils/sails/lib.ts new file mode 100644 index 000000000..7d22b1d3e --- /dev/null +++ b/frontend/apps/racing-car-game/src/app/utils/sails/lib.ts @@ -0,0 +1,526 @@ +import { TransactionBuilder, getServiceNamePrefix, getFnNamePrefix, ZERO_ADDRESS } from 'sails-js'; +import { GearApi, decodeAddress } from '@gear-js/api'; +import { TypeRegistry } from '@polkadot/types'; + +type ActorId = string; + +export interface InitConfig { + config: Config; +} + +export interface Config { + gas_to_remove_game: number | string | bigint; + gas_to_delete_session: number | string | bigint; + initial_speed: number; + min_speed: number; + max_speed: number; + gas_for_round: number | string | bigint; + time_interval: number; + max_distance: number; + time: number; + time_for_game_storage: number | string | bigint; + block_duration_ms: number | string | bigint; + gas_for_reply_deposit: number | string | bigint; + minimum_session_duration_ms: number | string | bigint; + s_per_block: number | string | bigint; +} + +export type StrategyAction = 'BuyAcceleration' | 'BuyShell' | 'Skip'; + +export interface Game { + cars: Record; + car_ids: Array; + current_turn: number; + state: GameState; + result: GameResult | null; + current_round: number; + last_time_step: number | string | bigint; +} + +export interface Car { + position: number; + speed: number; + car_actions: Array; + round_result: RoundAction | null; +} + +export type RoundAction = 'Accelerated' | 'SlowedDown' | 'SlowedDownAndAccelerated'; + +export type GameState = 'ReadyToStart' | 'Race' | 'Stopped' | 'Finished' | 'PlayerAction'; + +export type GameResult = 'Win' | 'Draw' | 'Lose'; + +export interface RoundInfo { + cars: Array<[ActorId, number, RoundAction | null]>; + result: GameResult | null; +} + +export interface SignatureData { + key: ActorId; + duration: number | string | bigint; + allowed_actions: Array; +} + +export type ActionsForSession = 'StartGame' | 'Move' | 'Skip'; + +export interface SessionData { + key: ActorId; + expires: number | string | bigint; + allowed_actions: Array; + expires_at_block: number; +} + +export class Program { + public readonly registry: TypeRegistry; + public readonly carRacesService: CarRacesService; + public readonly session: Session; + + constructor(public api: GearApi, public programId?: `0x${string}`) { + const types: Record = { + InitConfig: { config: 'Config' }, + Config: { + gas_to_remove_game: 'u64', + gas_to_delete_session: 'u64', + initial_speed: 'u32', + min_speed: 'u32', + max_speed: 'u32', + gas_for_round: 'u64', + time_interval: 'u32', + max_distance: 'u32', + time: 'u32', + time_for_game_storage: 'u64', + block_duration_ms: 'u64', + gas_for_reply_deposit: 'u64', + minimum_session_duration_ms: 'u64', + s_per_block: 'u64', + }, + StrategyAction: { _enum: ['BuyAcceleration', 'BuyShell', 'Skip'] }, + Game: { + cars: 'BTreeMap<[u8;32], Car>', + car_ids: 'Vec<[u8;32]>', + current_turn: 'u8', + state: 'GameState', + result: 'Option', + current_round: 'u32', + last_time_step: 'u64', + }, + Car: { position: 'u32', speed: 'u32', car_actions: 'Vec', round_result: 'Option' }, + RoundAction: { _enum: ['Accelerated', 'SlowedDown', 'SlowedDownAndAccelerated'] }, + GameState: { _enum: ['ReadyToStart', 'Race', 'Stopped', 'Finished', 'PlayerAction'] }, + GameResult: { _enum: ['Win', 'Draw', 'Lose'] }, + RoundInfo: { cars: 'Vec<([u8;32], u32, Option)>', result: 'Option' }, + SignatureData: { key: '[u8;32]', duration: 'u64', allowed_actions: 'Vec' }, + ActionsForSession: { _enum: ['StartGame', 'Move', 'Skip'] }, + SessionData: { + key: '[u8;32]', + expires: 'u64', + allowed_actions: 'Vec', + expires_at_block: 'u32', + }, + }; + + this.registry = new TypeRegistry(); + this.registry.setKnownTypes({ types }); + this.registry.register(types); + + this.carRacesService = new CarRacesService(this); + this.session = new Session(this); + } + + newCtorFromCode(code: Uint8Array | Buffer, init_config: InitConfig): TransactionBuilder { + const builder = new TransactionBuilder( + this.api, + this.registry, + 'upload_program', + ['New', init_config], + '(String, InitConfig)', + 'String', + code, + ); + + this.programId = builder.programId; + return builder; + } + + newCtorFromCodeId(codeId: `0x${string}`, init_config: InitConfig) { + const builder = new TransactionBuilder( + this.api, + this.registry, + 'create_program', + ['New', init_config], + '(String, InitConfig)', + 'String', + codeId, + ); + + this.programId = builder.programId; + return builder; + } +} + +export class CarRacesService { + constructor(private _program: Program) {} + + public addAdmin(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', + ['CarRacesService', 'AddAdmin', admin], + '(String, String, [u8;32])', + 'Null', + this._program.programId, + ); + } + + public addStrategyIds(car_ids: Array): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['CarRacesService', 'AddStrategyIds', car_ids], + '(String, String, Vec<[u8;32]>)', + 'Null', + this._program.programId, + ); + } + + public allowMessages(messages_allowed: boolean): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['CarRacesService', 'AllowMessages', messages_allowed], + '(String, String, bool)', + 'Null', + this._program.programId, + ); + } + + public playerMove(strategy_move: StrategyAction, session_for_account: ActorId | null): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['CarRacesService', 'PlayerMove', strategy_move, session_for_account], + '(String, String, StrategyAction, Option<[u8;32]>)', + 'Null', + this._program.programId, + ); + } + + public removeAdmin(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', + ['CarRacesService', 'RemoveAdmin', admin], + '(String, String, [u8;32])', + 'Null', + this._program.programId, + ); + } + + public removeGameInstance(account: 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', + ['CarRacesService', 'RemoveGameInstance', account], + '(String, String, [u8;32])', + 'Null', + this._program.programId, + ); + } + + public removeInstances(player_ids: Array | null): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['CarRacesService', 'RemoveInstances', player_ids], + '(String, String, Option>)', + 'Null', + this._program.programId, + ); + } + + public startGame(session_for_account: ActorId | null): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['CarRacesService', 'StartGame', session_for_account], + '(String, String, Option<[u8;32]>)', + 'Null', + this._program.programId, + ); + } + + public updateConfig(config: Config): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['CarRacesService', 'UpdateConfig', config], + '(String, String, Config)', + 'Null', + this._program.programId, + ); + } + + public async admins( + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise> { + const payload = this._program.registry.createType('(String, String)', ['CarRacesService', 'Admins']).toHex(); + const reply = await this._program.api.message.calculateReply({ + destination: this._program.programId!, + origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS, + payload, + value: value || 0, + gasLimit: this._program.api.blockGasLimit.toBigInt(), + at: atBlock, + }); + if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString()); + const result = this._program.registry.createType('(String, String, Vec<[u8;32]>)', reply.payload); + return result[2].toJSON() as unknown as Array; + } + + public async allGames( + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise> { + const payload = this._program.registry.createType('(String, String)', ['CarRacesService', 'AllGames']).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], Game)>)', reply.payload); + return result[2].toJSON() as unknown as Array<[ActorId, Game]>; + } + + public async configState( + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise { + const payload = this._program.registry.createType('(String, String)', ['CarRacesService', 'ConfigState']).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 game( + account_id: ActorId, + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise { + const payload = this._program.registry + .createType('(String, String, [u8;32])', ['CarRacesService', 'Game', 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 Game | null; + } + + public async messagesAllowed( + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise { + const payload = this._program.registry + .createType('(String, String)', ['CarRacesService', 'MessagesAllowed']) + .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; + } + + public async strategyIds( + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise> { + const payload = this._program.registry.createType('(String, String)', ['CarRacesService', 'StrategyIds']).toHex(); + const reply = await this._program.api.message.calculateReply({ + destination: this._program.programId!, + origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS, + payload, + value: value || 0, + gasLimit: this._program.api.blockGasLimit.toBigInt(), + at: atBlock, + }); + if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString()); + const result = this._program.registry.createType('(String, String, Vec<[u8;32]>)', reply.payload); + return result[2].toJSON() as unknown as Array; + } + + public subscribeToRoundInfoEvent(callback: (data: RoundInfo) => 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) === 'CarRacesService' && getFnNamePrefix(payload) === 'RoundInfo') { + callback( + this._program.registry + .createType('(String, String, RoundInfo)', message.payload)[2] + .toJSON() as unknown as RoundInfo, + ); + } + }); + } +} + +export class Session { + constructor(private _program: Program) {} + + public createSession(signature_data: SignatureData, signature: `0x${string}` | null): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Session', 'CreateSession', signature_data, signature], + '(String, String, SignatureData, Option>)', + 'Null', + this._program.programId, + ); + } + + public deleteSessionFromAccount(): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Session', 'DeleteSessionFromAccount'], + '(String, String)', + 'Null', + this._program.programId, + ); + } + + public deleteSessionFromProgram(session_for_account: 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', + ['Session', 'DeleteSessionFromProgram', session_for_account], + '(String, String, [u8;32])', + 'Null', + this._program.programId, + ); + } + + public async sessionForTheAccount( + account: ActorId, + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise { + const payload = this._program.registry + .createType('(String, String, [u8;32])', ['Session', 'SessionForTheAccount', account]) + .toHex(); + const reply = await this._program.api.message.calculateReply({ + destination: this._program.programId!, + origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS, + payload, + value: value || 0, + gasLimit: this._program.api.blockGasLimit.toBigInt(), + at: atBlock, + }); + if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString()); + const result = this._program.registry.createType('(String, String, Option)', reply.payload); + return result[2].toJSON() as unknown as SessionData | null; + } + + public async sessions( + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise> { + const payload = this._program.registry.createType('(String, String)', ['Session', 'Sessions']).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], SessionData)>)', reply.payload); + return result[2].toJSON() as unknown as Array<[ActorId, SessionData]>; + } + + public subscribeToSessionCreatedEvent(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) === 'Session' && getFnNamePrefix(payload) === 'SessionCreated') { + callback(null); + } + }); + } + + public subscribeToSessionDeletedEvent(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) === 'Session' && getFnNamePrefix(payload) === 'SessionDeleted') { + callback(null); + } + }); + } +} diff --git a/frontend/apps/racing-car-game/src/app/utils/sails/sails.ts b/frontend/apps/racing-car-game/src/app/utils/sails/sails.ts new file mode 100644 index 000000000..f99339f86 --- /dev/null +++ b/frontend/apps/racing-car-game/src/app/utils/sails/sails.ts @@ -0,0 +1,16 @@ +import { useProgram as useGearJsProgram } from '@gear-js/react-hooks'; +import { Program } from '../'; +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/racing-car-game/src/assets/meta/meta.txt b/frontend/apps/racing-car-game/src/assets/meta/meta.txt deleted file mode 100644 index 50e978fae..000000000 --- a/frontend/apps/racing-car-game/src/assets/meta/meta.txt +++ /dev/null @@ -1 +0,0 @@ -000200010000000000010400000001130000000000011600000000011f0000000120000000a13cc80008306361725f72616365735f696f2047616d65496e69740000040118636f6e666967040118436f6e66696700000408306361725f72616365735f696f18436f6e66696700002c01486761735f746f5f72656d6f76655f67616d6508010c753634000134696e697469616c5f73706565640c010c7533320001246d696e5f73706565640c010c7533320001246d61785f73706565640c010c7533320001346761735f666f725f726f756e6408010c75363400013474696d655f696e74657276616c0c010c7533320001306d61785f64697374616e63650c010c75333200011074696d650c010c75333200015474696d655f666f725f67616d655f73746f7261676508010c753634000144626c6f636b5f6475726174696f6e5f6d7308010c7536340001546761735f746f5f64656c6574655f73657373696f6e08010c75363400000800000506000c00000505001008306361725f72616365735f696f2847616d65416374696f6e0001342041646441646d696e040014011c4163746f7249640000002c52656d6f766541646d696e040014011c4163746f72496400010038416464537472617465677949647304011c6361725f6964732001305665633c4163746f7249643e00020024537461727447616d6504014c73657373696f6e5f666f725f6163636f756e7424013c4f7074696f6e3c4163746f7249643e00030010506c617904011c6163636f756e7414011c4163746f72496400040028506c617965724d6f766508014c73657373696f6e5f666f725f6163636f756e7424013c4f7074696f6e3c4163746f7249643e00013c73747261746567795f616374696f6e2801385374726174656779416374696f6e00050030557064617465436f6e6669672c01486761735f746f5f72656d6f76655f67616d652c012c4f7074696f6e3c7536343e000134696e697469616c5f737065656430012c4f7074696f6e3c7533323e0001246d696e5f737065656430012c4f7074696f6e3c7533323e0001246d61785f737065656430012c4f7074696f6e3c7533323e0001346761735f666f725f726f756e642c012c4f7074696f6e3c7536343e00013474696d655f696e74657276616c30012c4f7074696f6e3c7533323e0001306d61785f64697374616e636530012c4f7074696f6e3c7533323e00011074696d6530012c4f7074696f6e3c7533323e00015474696d655f666f725f67616d655f73746f726167652c012c4f7074696f6e3c7536343e000144626c6f636b5f6475726174696f6e5f6d732c012c4f7074696f6e3c7536343e0001546761735f746f5f64656c6574655f73657373696f6e2c012c4f7074696f6e3c7536343e0006004852656d6f766547616d65496e7374616e63650401286163636f756e745f696414011c4163746f7249640007004c52656d6f766547616d65496e7374616e63657304012c706c61796572735f6964733401504f7074696f6e3c5665633c4163746f7249643e3e00080034416c6c6f774d657373616765730400380110626f6f6c0009003443726561746553657373696f6e10010c6b657914011c4163746f7249640001206475726174696f6e08010c75363400013c616c6c6f7765645f616374696f6e733c01585665633c416374696f6e73466f7253657373696f6e3e0001247369676e617475726544013c4f7074696f6e3c5665633c75383e3e000a006044656c65746553657373696f6e46726f6d50726f6772616d04011c6163636f756e7414011c4163746f724964000b006044656c65746553657373696f6e46726f6d4163636f756e74000c00001410106773746418636f6d6d6f6e287072696d6974697665731c4163746f724964000004001801205b75383b2033325d000018000003200000001c001c00000503002000000214002404184f7074696f6e04045401140108104e6f6e6500000010536f6d6504001400000100002808306361725f72616365735f696f385374726174656779416374696f6e00010c3c427579416363656c65726174696f6e000000204275795368656c6c00010010536b6970000200002c04184f7074696f6e04045401080108104e6f6e6500000010536f6d6504000800000100003004184f7074696f6e040454010c0108104e6f6e6500000010536f6d6504000c00000100003404184f7074696f6e04045401200108104e6f6e6500000010536f6d6504002000000100003800000500003c00000240004008306361725f72616365735f696f44416374696f6e73466f7253657373696f6e00010824537461727447616d6500000028506c617965724d6f7665000100004404184f7074696f6e04045401480108104e6f6e6500000010536f6d650400480000010000480000021c004c0418526573756c740804540150044501540108084f6b040050000000000c45727204005400000100005008306361725f72616365735f696f2447616d655265706c790001303047616d6546696e69736865640000002c47616d65537461727465640001003453747261746567794164646564000200204d6f76654d6164650003004c47616d65496e7374616e636552656d6f76656400040040496e7374616e63657352656d6f7665640005002841646d696e41646465640006003041646d696e52656d6f76656400070034436f6e66696755706461746564000800545374617475734d65737361676573557064617465640009003853657373696f6e43726561746564000a003853657373696f6e44656c65746564000b00005408306361725f72616365735f696f2447616d654572726f72000118204e6f7441646d696e0000004c4d757374426554776f537472617465676965730001004847616d65416c726561647953746172746564000200344e6f74506c617965725475726e000300284e6f7450726f6772616d000400684d65737361676550726f63657373696e6753757370656e6465640005000058000004085c60005c08306361725f72616365735f696f345369676e61747572654461746100000c010c6b657914011c4163746f7249640001206475726174696f6e08010c75363400013c616c6c6f7765645f616374696f6e733c01585665633c416374696f6e73466f7253657373696f6e3e00006008306361725f72616365735f696f24526f756e64496e666f0000080110636172736401a05665633c284163746f7249642c207533322c204f7074696f6e3c526f756e64416374696f6e3e293e000118726573756c747401484f7074696f6e3c47616d65526573756c743e0000640000026800680000040c140c6c006c04184f7074696f6e04045401700108104e6f6e6500000010536f6d6504007000000100007008306361725f72616365735f696f2c526f756e64416374696f6e00010c2c416363656c65726174656400000028536c6f776564446f776e00010060536c6f776564446f776e416e64416363656c657261746564000200007404184f7074696f6e04045401780108104e6f6e6500000010536f6d6504007800000100007808306361725f72616365735f696f2847616d65526573756c7400010c0c57696e0000001044726177000100104c6f7365000200007c08306361725f72616365735f696f28537461746551756572790001201841646d696e730000002c53747261746567794964730001001047616d650401286163636f756e745f696414011c4163746f72496400020020416c6c47616d6573000300344d73674964546f47616d65496400040018436f6e6669670005003c4d65737361676573416c6c6f7765640006005053657373696f6e466f725468654163636f756e74040014011c4163746f724964000700008008306361725f72616365735f696f2853746174655265706c790001241841646d696e7304002001305665633c4163746f7249643e0000002c537472617465677949647304002001305665633c4163746f7249643e0001001047616d6504008401304f7074696f6e3c47616d653e00020020416c6c47616d65730400a401505665633c284163746f7249642c2047616d65293e000300344d73674964546f47616d6549640400ac01645665633c284d65737361676549642c204163746f724964293e0004002c57616974696e674d7367730400b8016c5665633c284d65737361676549642c204d6573736167654964293e00050018436f6e6669670400040118436f6e6669670006003c4d65737361676573416c6c6f7765640400380110626f6f6c0007005053657373696f6e466f725468654163636f756e740400c0013c4f7074696f6e3c53657373696f6e3e000800008404184f7074696f6e04045401880108104e6f6e6500000010536f6d6504008800000100008808306361725f72616365735f696f1047616d6500001c0110636172738c015842547265654d61703c4163746f7249642c204361723e00011c6361725f6964732001305665633c4163746f7249643e00013063757272656e745f7475726e1c010875380001147374617465a0012447616d655374617465000118726573756c747401484f7074696f6e3c47616d65526573756c743e00013463757272656e745f726f756e640c010c7533320001386c6173745f74696d655f7374657008010c75363400008c042042547265654d617008044b011404560190000400980000009008306361725f72616365735f696f0c4361720000100120706f736974696f6e0c010c75333200011473706565640c010c75333200012c6361725f616374696f6e739401405665633c526f756e64416374696f6e3e000130726f756e645f726573756c746c014c4f7074696f6e3c526f756e64416374696f6e3e0000940000027000980000029c009c00000408149000a008306361725f72616365735f696f2447616d655374617465000114305265616479546f537461727400000010526163650001001c53746f707065640002002046696e697368656400030030506c61796572416374696f6e00040000a4000002a800a800000408148800ac000002b000b000000408b41400b410106773746418636f6d6d6f6e287072696d697469766573244d6573736167654964000004001801205b75383b2033325d0000b8000002bc00bc00000408b4b400c004184f7074696f6e04045401c40108104e6f6e6500000010536f6d650400c40000010000c408306361725f72616365735f696f1c53657373696f6e000010010c6b657914011c4163746f72496400011c6578706972657308010c75363400013c616c6c6f7765645f616374696f6e733c01585665633c416374696f6e73466f7253657373696f6e3e000140657870697265735f61745f626c6f636b0c010c7533320000 \ No newline at end of file diff --git a/frontend/apps/racing-car-game/src/atoms.ts b/frontend/apps/racing-car-game/src/atoms.ts deleted file mode 100644 index b47fcff13..000000000 --- a/frontend/apps/racing-car-game/src/atoms.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { atom } from 'jotai'; -import { GameState, MsgIdToGameIdState } from './types'; - -export const CURRENT_GAME = atom(null); - -export const IS_CURRENT_GAME_READ_ATOM = atom(false); - -export const MSG_TO_GAME_ID = atom(null); - -export const IS_SUBSCRIBED_ATOM = atom(false); diff --git a/frontend/apps/racing-car-game/src/consts.ts b/frontend/apps/racing-car-game/src/consts.ts index 8cc31be12..de2a53e36 100644 --- a/frontend/apps/racing-car-game/src/consts.ts +++ b/frontend/apps/racing-car-game/src/consts.ts @@ -22,4 +22,4 @@ export const SEARCH_PARAMS = { MASTER_CONTRACT_ID: 'master', }; -export const SIGNLESS_ALLOWED_ACTIONS = ['StartGame', 'PlayerMove']; +export const SIGNLESS_ALLOWED_ACTIONS = ['StartGame', 'Move', 'Skip']; diff --git a/frontend/apps/racing-car-game/src/features/Game/atoms.ts b/frontend/apps/racing-car-game/src/features/Game/atoms.ts deleted file mode 100644 index 0f8870b94..000000000 --- a/frontend/apps/racing-car-game/src/features/Game/atoms.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { HexString } from '@gear-js/api'; -import { atom } from 'jotai'; -import { DecodedReply } from '@/types'; - -export const REPLY_DATA_ATOM = atom(null); - -export const IS_STATE_READ_ATOM = atom(false); - -export const CURRENT_SENT_MESSAGE_ID_ATOM = atom(null); - -export const IS_STARTING_NEW_GAME_ATOM = atom(false); diff --git a/frontend/apps/racing-car-game/src/features/Game/component/Heading/Heading.interface.ts b/frontend/apps/racing-car-game/src/features/Game/component/Heading/Heading.interface.ts index 573a10f72..89cbe55d0 100644 --- a/frontend/apps/racing-car-game/src/features/Game/component/Heading/Heading.interface.ts +++ b/frontend/apps/racing-car-game/src/features/Game/component/Heading/Heading.interface.ts @@ -1,7 +1,7 @@ -import { WinStatus } from '../Layout/Layout.interface'; +import { GameResult } from '@/app/utils'; export interface HeadingProps { currentTurn: string; isPlayerAction: boolean; - winStatus: WinStatus; + winStatus: GameResult | null; } diff --git a/frontend/apps/racing-car-game/src/features/Game/component/Layout/Layout.interface.ts b/frontend/apps/racing-car-game/src/features/Game/component/Layout/Layout.interface.ts deleted file mode 100644 index 1adc5dadf..000000000 --- a/frontend/apps/racing-car-game/src/features/Game/component/Layout/Layout.interface.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { GearCoreMessageUserUserMessage, HexString } from '@gear-js/api'; - -export interface LayoutProps { - currentTurn: string; -} - -export type WinStatus = 'Win' | 'Lose' | 'Draw' | null; - -export interface RepliesItem { - auto: GearCoreMessageUserUserMessage | null; - manual: GearCoreMessageUserUserMessage | null; -} - -export type RepliesQueue = RepliesItem[]; - -export type UserMessage = GearCoreMessageUserUserMessage & { - details?: MessageDetails; -}; - -export interface MessageDetails { - to?: HexString; - code?: { Error: any }; -} diff --git a/frontend/apps/racing-car-game/src/features/Game/component/Layout/Layout.tsx b/frontend/apps/racing-car-game/src/features/Game/component/Layout/Layout.tsx index 7fd40722d..fe2636f38 100644 --- a/frontend/apps/racing-car-game/src/features/Game/component/Layout/Layout.tsx +++ b/frontend/apps/racing-car-game/src/features/Game/component/Layout/Layout.tsx @@ -1,259 +1,52 @@ -import { memo, useCallback, useEffect, useRef, useState } from 'react'; +import { memo, useCallback, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { useAtom, useAtomValue } from 'jotai'; import isEqual from 'lodash.isequal'; -import { useAccount, useAlert, useApi, useHandleCalculateGas } from '@gear-js/react-hooks'; -import { UnsubscribePromise } from '@polkadot/api/types'; -import { UserMessageSent, decodeAddress } from '@gear-js/api'; +import { useAccount } from '@gear-js/react-hooks'; + import { Container, Footer } from '@dapps-frontend/ui'; -import { useCheckBalance, useDnsProgramIds } from '@dapps-frontend/hooks'; import { useEzTransactions } from '@dapps-frontend/ez-transactions'; import styles from './Layout.module.scss'; -import { cx, getDecodedReply, logger, withoutCommas } from '@/utils'; +import { cx } from '@/utils'; import { Heading } from '../Heading'; import { Road } from '../Road'; import { Button } from '@/ui'; import accelerateSVG from '@/assets/icons/accelerate-icon.svg'; import shootSVG from '@/assets/icons/shoot-icon.svg'; import { ReactComponent as GearLogoIcon } from '@/assets/icons/gear-logo-icon.svg'; -import { CURRENT_GAME, IS_CURRENT_GAME_READ_ATOM, IS_SUBSCRIBED_ATOM } from '@/atoms'; -import { usePlayerMoveMessage, useStartGameMessage } from '../../hooks'; import { Loader } from '@/components'; -import { MessageDetails, RepliesQueue, UserMessage, WinStatus } from './Layout.interface'; import { PLAY } from '@/App.routes'; -import { ContractError, DecodedReplyItem, GameState } from '@/types'; -import { ADDRESS } from '@/consts'; import { useAccountAvailableBalance } from '@/features/Wallet/hooks'; -import { - CURRENT_SENT_MESSAGE_ID_ATOM, - IS_STARTING_NEW_GAME_ATOM, - IS_STATE_READ_ATOM, - REPLY_DATA_ATOM, -} from '../../atoms'; +import { useEventRoundInfoSubscription, usePlayerMoveMessage, useStartGameMessage, useGameQuery } from '../../sails'; +import { GameResult } from '@/app/utils'; function LayoutComponent() { const { signless, gasless } = useEzTransactions(); - const [currentGame, setCurrentGame] = useAtom(CURRENT_GAME); - const isCurrentGameRead = useAtomValue(IS_CURRENT_GAME_READ_ATOM); + const { game: currentGame, refetch } = useGameQuery(); + const isCurrentGameRead = currentGame !== undefined; const [isPlayerAction, setIsPlayerAction] = useState(true); - const [isLoading, setIsLoading] = useAtom(IS_STARTING_NEW_GAME_ATOM); + const [isLoading, setIsLoading] = useState(false); const [isRoadLoaded, setIsRoadLoaded] = useState(false); const { isAvailableBalanceReady } = useAccountAvailableBalance(); const { account } = useAccount(); - const alert = useAlert(); - const { checkBalance } = useCheckBalance({ - signlessPairVoucherId: signless.voucher?.id, - gaslessVoucherId: gasless.voucherId, - }); const navigate = useNavigate(); - const sendPlayerMoveMessage = usePlayerMoveMessage(); - const { meta, message: startGameMessage } = useStartGameMessage(); - const { programId } = useDnsProgramIds(); - const calculateGas = useHandleCalculateGas(programId, meta); - const [isStateRead, setIsStateRead] = useAtom(IS_STATE_READ_ATOM); - const { api } = useApi(); - - const messageSubscription: React.MutableRefObject = useRef(null); - const repliesQueue: React.MutableRefObject = useRef([]); - const [replyData, setReplyData] = useAtom(REPLY_DATA_ATOM); - const [currentSentMessageId, setCurrentSentMessageId] = useAtom(CURRENT_SENT_MESSAGE_ID_ATOM); - const [isSubscribed, setIsSubscribed] = useAtom(IS_SUBSCRIBED_ATOM); - - const handleUnsubscribeFromEvent = (onSuccess?: () => void) => { - if (messageSubscription.current) { - messageSubscription.current?.then((unsubCallback) => { - unsubCallback(); - logger('UNsubscribed from reply'); - setIsSubscribed(false); - onSuccess?.(); - }); - } - }; - - const decodePair = useCallback( - (i: number) => { - logger('triggers SentMessageId Effect'); - - if (i > 2) { - setIsStateRead(false); - setIsLoading(false); - } + const { playerMoveMessage } = usePlayerMoveMessage(); + const { startGameMessage } = useStartGameMessage(); - if (currentSentMessageId) { - logger(`SentMessageId exists: ${currentSentMessageId}`); - logger(repliesQueue.current); - const foundRepliesPair = repliesQueue.current.find( - (item) => (item.auto?.toHuman().details as MessageDetails).to === currentSentMessageId, - ); + const subscriptionCallback = useCallback(() => { + refetch(); + setIsPlayerAction(true); + }, []); - logger(`Reply Pair found:`); - logger({ auto: foundRepliesPair?.auto?.toHuman(), manual: foundRepliesPair?.manual?.toHuman() }); - logger(`Reply found: ${foundRepliesPair?.manual}`); - - if (foundRepliesPair?.auto?.toHuman() && foundRepliesPair.manual?.toHuman()) { - const { manual } = foundRepliesPair; - - logger('trying to decode....:'); - try { - const reply = getDecodedReply(manual.payload, meta); - logger('DECODED message successfully'); - logger('new reply HAS COME:'); - logger(reply); - - if (reply && reply.cars.length && !isEqual(reply?.cars, replyData?.cars)) { - logger('prev reply state:'); - logger(replyData); - logger('new reply UPDATED and going to state:'); - logger(reply); - setReplyData(reply); - setCurrentSentMessageId(null); - handleUnsubscribeFromEvent(); - } - } catch (e) { - logger(e); - alert.error((e as ContractError).message); - } - } - - if (foundRepliesPair && foundRepliesPair.auto?.toHuman() && !foundRepliesPair.manual?.toHuman()) { - setCurrentSentMessageId(null); - setIsPlayerAction(true); - handleUnsubscribeFromEvent(); - - if (isLoading) { - setIsStateRead(false); - setIsLoading(false); - } - } - - if (!foundRepliesPair?.auto?.toHuman()) { - console.log(`reply not found, retrying(${i + 1})`); - setTimeout(() => decodePair(i + 1), 2000); - } - } - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [currentSentMessageId], - ); - - useEffect(() => { - decodePair(0); - }, [decodePair]); - - const handleChangeState = ({ data: _data }: UserMessageSent) => { - const { message } = _data; - - const { destination, source, details: messageDetails, id } = message as UserMessage; - - const signlessPairAddress = signless.pair && decodeAddress(signless.pair.address); - const isOwner = destination.toHex() === account?.decodedAddress || destination.toHex() === signlessPairAddress; - const isCurrentProgram = source.toHex() === programId; - - const details = messageDetails.toHuman() as MessageDetails; - - if (isOwner && isCurrentProgram) { - if (details?.to && !repliesQueue.current.map((item) => item.auto?.toHuman().id).includes(id.toHex())) { - console.log('pushed'); - console.log(repliesQueue.current.map((item) => (item.auto?.toHuman()?.details as MessageDetails).to)); - - repliesQueue.current.push({ auto: message, manual: null }); - } - - if (!details && !repliesQueue.current[repliesQueue.current.length - 1].manual) { - console.log('pushed2'); - - repliesQueue.current[repliesQueue.current.length - 1].manual = message; - } - logger(repliesQueue.current.map((item) => ({ auto: item.auto?.toHuman(), manual: item.manual?.toHuman() }))); - } - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - - const handleSubscribeToEvent = useCallback(async () => { - if (api && meta && !isSubscribed) { - messageSubscription.current = api.gearEvents.subscribeToGearEvent('UserMessageSent', handleChangeState); - setIsSubscribed(true); - logger('Subscribed on reply'); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [api, isSubscribed, meta, signless.pair?.address]); - - const defineStrategyAction = (type: 'accelerate' | 'shoot') => { - if (type === 'accelerate') { - return 'BuyAcceleration'; - } - - if (type === 'shoot') { - return 'BuyShell'; - } - }; + useEventRoundInfoSubscription(subscriptionCallback); const handleActionChoose = async (type: 'accelerate' | 'shoot') => { setIsPlayerAction(false); - logger(`CLICK ACTION ${type}`); - logger(`Disabling actions`); - const payload = { - PlayerMove: { - strategy_action: defineStrategyAction(type), - }, - }; - handleSubscribeToEvent(); - - let { voucherId } = gasless; - if (account && gasless.isEnabled && !gasless.voucherId && !signless.isActive) { - voucherId = await gasless.requestVoucher(account.address); - } - - const getOnError = (error: string) => () => { - setIsPlayerAction(true); - handleUnsubscribeFromEvent(); - logger(error); - }; - - calculateGas(payload) - .then((res) => res.toHuman()) - .then(({ min_limit }) => { - const minLimit = withoutCommas(min_limit as string); - const gasLimit = Math.floor(Number(minLimit) + Number(minLimit) * 0.2); - logger(`Calculating gas:`); - logger(`MIN_LIMIT ${min_limit}`); - logger(`LIMIT ${gasLimit}`); - logger(`Calculated gas SUCCESS`); - logger(`Sending message`); - console.log(`START TURN ${Number(currentGame?.currentRound) + 1}`); - - const sendMessage = () => - sendPlayerMoveMessage({ - payload, - gasLimit, - voucherId, - onError: getOnError(`Errror send message`), - onSuccess: (messageId) => { - logger(`sucess on ID: ${messageId}`); - }, - onInBlock: (messageId) => { - logger('messageInBlock'); - logger(`messageID: ${messageId}`); - setCurrentSentMessageId(messageId); - }, - }); - - if (voucherId) { - sendMessage(); - } else { - checkBalance(gasLimit, sendMessage, getOnError(`Errror check balance`)); - } - }) - .catch((error) => { - logger(error); - setIsPlayerAction(true); - handleUnsubscribeFromEvent(); - alert.error('Gas calculation error'); - }); + const strategyActionMap = { accelerate: 'BuyAcceleration' as const, shoot: 'BuyShell' as const }; + playerMoveMessage(strategyActionMap[type], { onError: () => setIsPlayerAction(true) }); }; - const defineWinStatus = (): WinStatus => { + const defineWinStatus = (): GameResult | null => { if (currentGame?.state === 'Finished') { return currentGame.result; } @@ -263,115 +56,30 @@ function LayoutComponent() { const handleStartNewGame = useCallback( async (startManually?: boolean) => { - if (meta && isCurrentGameRead && !isLoading && (!currentGame || startManually)) { - const payload = { - StartGame: {}, - }; - - handleSubscribeToEvent(); - - const onError = (error?: unknown) => { - handleUnsubscribeFromEvent(); - setIsStateRead(true); + console.log('handleStartNewGame', isCurrentGameRead && !isLoading && (!currentGame || startManually)); + if (isCurrentGameRead && !isLoading && (!currentGame || startManually)) { + const onError = () => { setIsLoading(false); - logger(error || 'error'); navigate(PLAY, { replace: true }); }; - setIsPlayerAction(false); + setIsPlayerAction(true); setIsLoading(true); - setIsStateRead(false); - - let { voucherId } = gasless; - if (account && gasless.isEnabled && !gasless.voucherId && !signless.isActive) { - voucherId = await gasless.requestVoucher(account.address); - } - calculateGas(payload) - .then((res) => res.toHuman()) - .then(({ min_limit }) => { - const minLimit = withoutCommas(min_limit as string); - const gasLimit = Math.floor(Number(minLimit) + Number(minLimit) * 0.2); - - const sendMessage = () => { - startGameMessage({ - payload, - gasLimit, - voucherId, - onInBlock: (messageId) => { - logger('Start Game messageInBlock'); - logger(`messageID: ${messageId}`); - setCurrentSentMessageId(messageId); - }, - onError, - }); - }; - - if (voucherId) { - sendMessage(); - } else { - checkBalance(gasLimit, sendMessage, onError); - } - }) - .catch((error) => { - alert.error('Gas calculation error'); - onError(error); - }); + await startGameMessage({ onError }); + console.log('refetch'); + await refetch(); + setIsLoading(false); } }, // eslint-disable-next-line react-hooks/exhaustive-deps - [meta, currentGame, isCurrentGameRead, account, gasless, signless, handleSubscribeToEvent], + [currentGame, isCurrentGameRead, account, gasless, signless], ); - useEffect(() => { - if (isStateRead) { - setIsPlayerAction(true); - } - }, [isStateRead]); - useEffect(() => { handleStartNewGame(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [meta, isCurrentGameRead]); - - useEffect(() => { - if (replyData && currentGame) { - const { cars, result } = replyData; - logger('Updates state to new reply'); - - setCurrentGame(() => - cars.reduce((acc: GameState, item: DecodedReplyItem) => { - const [address, position, effect] = item; - - return { - ...acc, - cars: { - ...acc.cars, - [address]: { - ...acc.cars[address], - position, - roundResult: effect, - }, - }, - }; - }, currentGame), - ); - setCurrentGame((prev) => - prev - ? { - ...prev, - result, - state: result ? 'Finished' : prev.state, - currentRound: String(Number(prev.currentRound) + 1), - } - : null, - ); - logger('Enabling actions'); - setIsPlayerAction(true); - logger(`END OF TURN ${Number(currentGame.currentRound) + 1}`); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [replyData]); + }, [isCurrentGameRead]); const handleRoadLoaded = () => { setIsRoadLoaded(true); @@ -379,16 +87,16 @@ function LayoutComponent() { return ( <> - {currentGame && account?.decodedAddress && isAvailableBalanceReady && !isLoading && isStateRead ? ( + {currentGame && account?.decodedAddress && isAvailableBalanceReady && !isLoading ? (
{isRoadLoaded && ( )} - + {isRoadLoaded && ( <> {currentGame.state !== 'Finished' && ( @@ -399,7 +107,7 @@ function LayoutComponent() { size="large" icon={accelerateSVG} disabled={!isPlayerAction} - isLoading={!account.decodedAddress || !meta} + isLoading={!account.decodedAddress} className={cx(styles['control-button'])} onClick={() => handleActionChoose('accelerate')} /> @@ -409,7 +117,7 @@ function LayoutComponent() { size="large" icon={shootSVG} disabled={!isPlayerAction} - isLoading={!account.decodedAddress || !meta} + isLoading={!account.decodedAddress} className={cx(styles['control-button'], styles['control-button-red'])} onClick={() => handleActionChoose('shoot')} /> diff --git a/frontend/apps/racing-car-game/src/features/Game/component/Road/Road.interface.ts b/frontend/apps/racing-car-game/src/features/Game/component/Road/Road.interface.ts index 0163635ea..1657035ad 100644 --- a/frontend/apps/racing-car-game/src/features/Game/component/Road/Road.interface.ts +++ b/frontend/apps/racing-car-game/src/features/Game/component/Road/Road.interface.ts @@ -1,4 +1,5 @@ -import { Car, Cars } from '@/types'; +import { Car } from '@/app/utils'; +import { Cars } from '@/types'; export type CarEffect = 'shooted' | 'accelerated' | 'sAndA' | null; export interface RoadProps { diff --git a/frontend/apps/racing-car-game/src/features/Game/component/Road/Road.tsx b/frontend/apps/racing-car-game/src/features/Game/component/Road/Road.tsx index 1dbe52a4d..45c2d85c8 100644 --- a/frontend/apps/racing-car-game/src/features/Game/component/Road/Road.tsx +++ b/frontend/apps/racing-car-game/src/features/Game/component/Road/Road.tsx @@ -1,7 +1,7 @@ import { MutableRefObject, memo, useEffect, useRef, useState } from 'react'; import isEqual from 'lodash.isequal'; import styles from './Road.module.scss'; -import { cx, withoutCommas } from '@/utils'; +import { cx } from '@/utils'; import startSVG from '@/assets/icons/game-start-icon.svg'; import finishSVG from '@/assets/icons/game-finish-icon.svg'; import roadLineSVG from '@/assets/icons/road-line-svg.svg'; @@ -81,10 +81,10 @@ function RoadComponent({ newCars, carIds, onRoadLoaded }: RoadProps) { ...acc, [id]: { ...newCars[id], - speed: Number(withoutCommas(newCars[id].speed)), - position: Number(withoutCommas(newCars[id].position)) + carDistanceFromInit, + speed: newCars[id].speed, + position: newCars[id].position + carDistanceFromInit, positionY: carPositionsY[i], - effect: defineCarEffect(newCars[id].roundResult), + effect: defineCarEffect(newCars[id].round_result), }, }), {}, @@ -101,9 +101,9 @@ function RoadComponent({ newCars, carIds, onRoadLoaded }: RoadProps) { ...prev, [id]: { ...prev[id], - speed: Number(withoutCommas(newCars[id].speed)), - position: Number(withoutCommas(newCarsToUpdate[id].position)) + carDistanceFromInit, - effect: defineCarEffect(newCarsToUpdate[id].roundResult), + speed: newCars[id].speed, + position: newCarsToUpdate[id].position + carDistanceFromInit, + effect: defineCarEffect(newCarsToUpdate[id].round_result), }, } : null, diff --git a/frontend/apps/racing-car-game/src/features/Game/hooks.ts b/frontend/apps/racing-car-game/src/features/Game/hooks.ts deleted file mode 100644 index e3047ab4d..000000000 --- a/frontend/apps/racing-car-game/src/features/Game/hooks.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; -import { useAtom, useSetAtom } from 'jotai'; -import { UserMessageSent } from '@gear-js/api'; -import { useAccount, useAlert, useApi } from '@gear-js/react-hooks'; -import { UnsubscribePromise } from '@polkadot/api/types'; -import isEqual from 'lodash.isequal'; -import { useSignlessSendMessage } from '@dapps-frontend/ez-transactions'; -import { useDnsProgramIds } from '@dapps-frontend/hooks'; -import { useProgramMetadata } from '@/hooks'; -import metaTxt from '@/assets/meta/meta.txt'; -import { ContractError, GameState } from '@/types'; -import { CURRENT_SENT_MESSAGE_ID_ATOM, IS_STATE_READ_ATOM, REPLY_DATA_ATOM } from './atoms'; -import { getDecodedReply, logger } from '@/utils'; -import { IS_SUBSCRIBED_ATOM } from '@/atoms'; - -function usePlayerMoveMessage() { - const { programId } = useDnsProgramIds(); - const meta = useProgramMetadata(metaTxt); - - return useSignlessSendMessage(programId, meta, { disableAlerts: true }); -} - -function useStartGameMessage() { - const { programId } = useDnsProgramIds(); - const meta = useProgramMetadata(metaTxt); - - const message = useSignlessSendMessage(programId, meta, { disableAlerts: true }); - - return { meta, message }; -} - -function useSubscribeSentMessage() { - const { api } = useApi(); - const alert = useAlert(); - const messageSubscription: React.MutableRefObject = useRef(null); - const meta = useProgramMetadata(metaTxt); - const { account } = useAccount(); - const [replyData, setReplyData] = useAtom(REPLY_DATA_ATOM); - const [currentSentMessageId, setCurrentSentMessageId] = useAtom(CURRENT_SENT_MESSAGE_ID_ATOM); - const [isSubscribed, setIsSubscribed] = useAtom(IS_SUBSCRIBED_ATOM); - const { programId } = useDnsProgramIds(); - - const handleUnsubscribeFromEvent = (onSuccess?: () => void) => { - if (messageSubscription.current) { - messageSubscription.current?.then((unsubCallback) => { - unsubCallback(); - logger('UNsubscribed from reply'); - onSuccess?.(); - }); - } - }; - - const clearReplyData = () => { - setReplyData(null); - }; - - const handleChangeState = useCallback( - ({ data: _data }: UserMessageSent) => { - const { message } = _data; - const { destination, source, payload } = message; - const isOwner = destination.toHex() === account?.decodedAddress; - const isCurrentProgram = source.toHex() === programId; - // const isNeededMessageId = id.toHex() === currentSentMessageId; - - if (isOwner && isCurrentProgram) { - logger('new message:'); - logger(message.toHuman()); - logger('trying to decode....:'); - try { - const reply = getDecodedReply(payload, meta); - logger('DECODED message successfully'); - logger('new reply HAS COME:'); - logger(reply); - - if (reply && reply.cars.length && !isEqual(reply?.cars, replyData?.cars)) { - logger('prev reply state:'); - logger(replyData); - logger('new reply UPDATED and going to state:'); - logger(reply); - setReplyData(reply); - setCurrentSentMessageId(null); - // handleUnsubscribeFromEvent(); - } - } catch (e) { - logger(e); - alert.error((e as ContractError).message); - } - } - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [replyData, meta], - ); - - const handleSubscribeToEvent = () => { - if (api) { - messageSubscription.current = api.gearEvents.subscribeToGearEvent('UserMessageSent', handleChangeState); - logger('subscribed on reply'); - } - }; - - useEffect(() => { - if (meta && !isSubscribed) { - handleSubscribeToEvent(); - setIsSubscribed(true); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [meta, isSubscribed]); - - return { handleSubscribeToEvent, handleUnsubscribeFromEvent, replyData, clearReplyData }; -} - -function useGameState() { - const { programId } = useDnsProgramIds(); - const { api } = useApi(); - const meta = useProgramMetadata(metaTxt); - const [gameData, setGameData] = useState(null); - const setReplyData = useSetAtom(REPLY_DATA_ATOM); - const [isStateRead, setIsStateRead] = useAtom(IS_STATE_READ_ATOM); - const { account, isAccountReady } = useAccount(); - - const handleReadState = useCallback(async () => { - if (api && meta && programId && isAccountReady && !isStateRead) { - try { - const res = await api.programState.read( - { - programId, - payload: { - Game: { - account_id: account?.decodedAddress, - }, - }, - }, - meta, - ); - logger('state init'); - const state = (await res.toHuman()) as any; - setGameData(state.Game); - setIsStateRead(true); - } catch (err) { - logger(err); - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [meta, api, programId, isAccountReady, isStateRead, account?.decodedAddress]); - - useEffect(() => { - if (account?.decodedAddress) { - setIsStateRead(false); - setReplyData(null); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [account?.decodedAddress]); - - useEffect(() => { - handleReadState(); - }, [handleReadState]); - - return { state: { Game: gameData || null }, isStateRead, setIsStateRead }; -} - -export { usePlayerMoveMessage, useStartGameMessage, useGameState, useSubscribeSentMessage }; diff --git a/frontend/apps/racing-car-game/src/features/Game/sails/events/index.ts b/frontend/apps/racing-car-game/src/features/Game/sails/events/index.ts new file mode 100644 index 000000000..97a7cfd79 --- /dev/null +++ b/frontend/apps/racing-car-game/src/features/Game/sails/events/index.ts @@ -0,0 +1 @@ +export { useEventRoundInfoSubscription } from './use-event-round-info-subscription'; diff --git a/frontend/apps/racing-car-game/src/features/Game/sails/events/use-event-round-info-subscription.ts b/frontend/apps/racing-car-game/src/features/Game/sails/events/use-event-round-info-subscription.ts new file mode 100644 index 000000000..9312a5e70 --- /dev/null +++ b/frontend/apps/racing-car-game/src/features/Game/sails/events/use-event-round-info-subscription.ts @@ -0,0 +1,13 @@ +import { useProgramEvent } from '@gear-js/react-hooks'; +import { RoundInfo, useProgram } from '@/app/utils'; + +export function useEventRoundInfoSubscription(onData: (info: RoundInfo) => void) { + const program = useProgram(); + + useProgramEvent({ + program, + serviceName: 'carRacesService', + functionName: 'subscribeToRoundInfoEvent', + onData, + }); +} diff --git a/frontend/apps/racing-car-game/src/features/Game/sails/index.ts b/frontend/apps/racing-car-game/src/features/Game/sails/index.ts new file mode 100644 index 000000000..35fc718a8 --- /dev/null +++ b/frontend/apps/racing-car-game/src/features/Game/sails/index.ts @@ -0,0 +1,3 @@ +export * from './events'; +export * from './messages'; +export * from './queries'; diff --git a/frontend/apps/racing-car-game/src/features/Game/sails/messages/index.ts b/frontend/apps/racing-car-game/src/features/Game/sails/messages/index.ts new file mode 100644 index 000000000..da5ed537f --- /dev/null +++ b/frontend/apps/racing-car-game/src/features/Game/sails/messages/index.ts @@ -0,0 +1,2 @@ +export { useStartGameMessage } from './use-start-game-message'; +export { usePlayerMoveMessage } from './use-player-move-message'; diff --git a/frontend/apps/racing-car-game/src/features/Game/sails/messages/use-player-move-message.ts b/frontend/apps/racing-car-game/src/features/Game/sails/messages/use-player-move-message.ts new file mode 100644 index 000000000..4a050b47f --- /dev/null +++ b/frontend/apps/racing-car-game/src/features/Game/sails/messages/use-player-move-message.ts @@ -0,0 +1,33 @@ +import { useSendProgramTransaction } from '@gear-js/react-hooks'; +import { usePrepareEzTransactionParams } from '@dapps-frontend/ez-transactions'; +import { StrategyAction, useProgram } from '@/app/utils'; + +type Options = { + onError?: () => void; +}; + +export const usePlayerMoveMessage = () => { + const program = useProgram(); + const { sendTransactionAsync } = useSendProgramTransaction({ + program, + serviceName: 'carRacesService', + functionName: 'playerMove', + }); + const { prepareEzTransactionParams } = usePrepareEzTransactionParams(); + + const playerMoveMessage = async (strategyMove: StrategyAction, { onError }: Options) => { + try { + const { sessionForAccount, ...params } = await prepareEzTransactionParams(); + const { result } = await sendTransactionAsync({ + args: [strategyMove, sessionForAccount], + ...params, + }); + return result.response(); + } catch (e) { + onError?.(); + console.error(e); + } + }; + + return { playerMoveMessage }; +}; diff --git a/frontend/apps/racing-car-game/src/features/Game/sails/messages/use-start-game-message.ts b/frontend/apps/racing-car-game/src/features/Game/sails/messages/use-start-game-message.ts new file mode 100644 index 000000000..bf6270a09 --- /dev/null +++ b/frontend/apps/racing-car-game/src/features/Game/sails/messages/use-start-game-message.ts @@ -0,0 +1,33 @@ +import { useSendProgramTransaction } from '@gear-js/react-hooks'; +import { usePrepareEzTransactionParams } from '@dapps-frontend/ez-transactions'; +import { useProgram } from '@/app/utils'; + +type Options = { + onError?: () => void; +}; + +export const useStartGameMessage = () => { + const program = useProgram(); + const { sendTransactionAsync } = useSendProgramTransaction({ + program, + serviceName: 'carRacesService', + functionName: 'startGame', + }); + const { prepareEzTransactionParams } = usePrepareEzTransactionParams(); + + const startGameMessage = async ({ onError }: Options) => { + try { + const { sessionForAccount, ...params } = await prepareEzTransactionParams(); + const { result } = await sendTransactionAsync({ + args: [sessionForAccount], + ...params, + }); + return result.response(); + } catch (e) { + onError?.(); + console.error(e); + } + }; + + return { startGameMessage }; +}; diff --git a/frontend/apps/racing-car-game/src/features/Game/sails/queries/index.ts b/frontend/apps/racing-car-game/src/features/Game/sails/queries/index.ts new file mode 100644 index 000000000..f8abe9794 --- /dev/null +++ b/frontend/apps/racing-car-game/src/features/Game/sails/queries/index.ts @@ -0,0 +1 @@ +export { useGameQuery } from './use-game-query'; diff --git a/frontend/apps/racing-car-game/src/features/Game/sails/queries/use-game-query.ts b/frontend/apps/racing-car-game/src/features/Game/sails/queries/use-game-query.ts new file mode 100644 index 000000000..be2449c7b --- /dev/null +++ b/frontend/apps/racing-car-game/src/features/Game/sails/queries/use-game-query.ts @@ -0,0 +1,17 @@ +import { useProgram } from '@/app/utils'; +import { useAccount, useProgramQuery } from '@gear-js/react-hooks'; + +export const useGameQuery = () => { + const program = useProgram(); + const { account } = useAccount(); + + const { data, refetch, isFetching, error } = useProgramQuery({ + program, + serviceName: 'carRacesService', + functionName: 'game', + args: [account?.decodedAddress!], + query: { enabled: account ? undefined : false }, + }); + + return { game: data, isFetching, refetch, error }; +}; diff --git a/frontend/apps/racing-car-game/src/features/Main/components/Layout/Layout.tsx b/frontend/apps/racing-car-game/src/features/Main/components/Layout/Layout.tsx index 998d45c76..77342c416 100644 --- a/frontend/apps/racing-car-game/src/features/Main/components/Layout/Layout.tsx +++ b/frontend/apps/racing-car-game/src/features/Main/components/Layout/Layout.tsx @@ -1,29 +1,24 @@ -import { useAtomValue } from 'jotai'; import { useNavigate } from 'react-router-dom'; import { useAccount } from '@gear-js/react-hooks'; import { EzTransactionsSwitch } from '@dapps-frontend/ez-transactions'; import { Button } from '@/ui'; -import { CURRENT_GAME } from '@/atoms'; import { START } from '@/App.routes'; import { Welcome } from '@/features/Main/components'; import styles from './Layout.module.scss'; import { cx } from '@/utils'; -import metaTxt from '@/assets/meta/meta.txt'; -import { useProgramMetadata } from '@/hooks'; import { useAccountAvailableBalance } from '@/features/Wallet/hooks'; -import { IS_STATE_READ_ATOM } from '@/features/Game/atoms'; import { SIGNLESS_ALLOWED_ACTIONS } from '@/consts'; +import { useGameQuery } from '@/features/Game/sails'; function Layout() { const navigate = useNavigate(); - const currentGame = useAtomValue(CURRENT_GAME); - const isStateRead = useAtomValue(IS_STATE_READ_ATOM); + + const { game, isFetching } = useGameQuery(); const { account } = useAccount(); - const meta = useProgramMetadata(metaTxt); const { isAvailableBalanceReady, availableBalance } = useAccountAvailableBalance(); const handleGoToPlay = async () => { - if (isAvailableBalanceReady && account?.decodedAddress && meta) { + if (isAvailableBalanceReady && account?.decodedAddress) { navigate(START, { replace: true }); } }; @@ -31,12 +26,12 @@ function Layout() { return ( - - {open && ( -
-
    - {Object.keys(menu).map((item) => ( -
  • - -
  • - ))} -
-
- )} -
- ); -} - -export { Dropdown }; diff --git a/frontend/apps/racing-car-game/src/ui/Dropdown/index.ts b/frontend/apps/racing-car-game/src/ui/Dropdown/index.ts deleted file mode 100644 index 2f29bad4e..000000000 --- a/frontend/apps/racing-car-game/src/ui/Dropdown/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Dropdown'; diff --git a/frontend/apps/racing-car-game/src/ui/index.ts b/frontend/apps/racing-car-game/src/ui/index.ts index a77992d0d..45d12f779 100644 --- a/frontend/apps/racing-car-game/src/ui/index.ts +++ b/frontend/apps/racing-car-game/src/ui/index.ts @@ -1,5 +1,4 @@ export * from './Button'; -export * from './Dropdown'; export * from './Link'; export * from './Input'; export * from './Alert'; diff --git a/frontend/apps/racing-car-game/src/utils.ts b/frontend/apps/racing-car-game/src/utils.ts index abb632347..3e3b3f13c 100644 --- a/frontend/apps/racing-car-game/src/utils.ts +++ b/frontend/apps/racing-car-game/src/utils.ts @@ -1,12 +1,7 @@ -import { ProgramMetadata } from '@gear-js/api'; -import { SignlessTransactionsMetadataProviderProps } from '@dapps-frontend/signless-transactions'; import { AlertContainerFactory } from '@gear-js/react-hooks/dist/esm/types'; -import { Bytes } from '@polkadot/types'; -import { Codec } from '@polkadot/types/types'; import clsx from 'clsx'; import { useEffect } from 'react'; import { useLocation } from 'react-router-dom'; -import { DecodedReply } from './types'; export const cx = (...styles: string[]) => clsx(...styles); @@ -56,14 +51,6 @@ export const copyToClipboard = async ({ } }; -export const get = (url: string) => - fetch(url, { - method: 'GET', - }).then(async (res) => { - const json = await res.json(); - return json as T; - }); - export function ScrollToTop() { const { pathname } = useLocation(); @@ -77,54 +64,6 @@ export function ScrollToTop() { return null; } -export const withoutCommas = (value: string) => (typeof value === 'string' ? value.replace(/,/g, '') : value); - export const isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( navigator.userAgent, ); - -export const logger = (message: unknown | unknown[]) => { - const date = new Date(); - let milliseconds = ''; - const milli = date.getMilliseconds(); - - if (milli < 10) { - milliseconds = `00${milli}`; - } else if (milli < 100) { - milliseconds = `0${milli}`; - } else { - milliseconds = `${milli}`; - } - - const time = `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}.${milliseconds}`; - - console.log(time, message); -}; - -/** - * Get first element of tuple type - * */ -export const createSignatureType: SignlessTransactionsMetadataProviderProps['createSignatureType'] = ( - metadata, - payloadToSign, -) => { - if (!metadata.types?.others?.output) { - throw new Error(`Metadata type doesn't exist`); - } - - const data = metadata.createType(metadata.types.others.output, [payloadToSign]) as unknown as Codec[]; - return data[0].toHex(); -}; - -const getDecodedPayload = (payload: Bytes, meta?: ProgramMetadata) => { - if (meta?.types.others.output) { - return meta.createType(meta?.types.others.output, [null, payload]).toHuman() as [null, DecodedReply]; - } -}; - -export const getDecodedReply = (payload: Bytes, meta?: ProgramMetadata) => { - const decodedPayload = getDecodedPayload(payload, meta); - if (decodedPayload) { - return decodedPayload[1] as DecodedReply; - } -}; diff --git a/frontend/dev/gstd-racing-car-game/.env.example b/frontend/dev/gstd-racing-car-game/.env.example new file mode 100644 index 000000000..132100370 --- /dev/null +++ b/frontend/dev/gstd-racing-car-game/.env.example @@ -0,0 +1,10 @@ +REACT_APP_NODE_ADDRESS= +REACT_APP_DNS_API_URL= +REACT_APP_DNS_NAME= +REACT_APP_GTM_ID_CARS= +REACT_APP_GASLESS_BACKEND_ADDRESS= + +# optional, specify sentry dsn and targetted domain for error tracking +# if domain is not specified, localhost is used by default +REACT_APP_SENTRY_DSN= +REACT_APP_SENTRY_TARGET= diff --git a/frontend/dev/gstd-racing-car-game/.eslintrc.js b/frontend/dev/gstd-racing-car-game/.eslintrc.js new file mode 100644 index 000000000..9e5baa593 --- /dev/null +++ b/frontend/dev/gstd-racing-car-game/.eslintrc.js @@ -0,0 +1,46 @@ +module.exports = { + root: true, + env: { + browser: true, + es2021: true, + }, + extends: [ + 'plugin:react/recommended', + 'airbnb', + 'airbnb/hooks', + 'airbnb-typescript', + 'plugin:react/jsx-runtime', + 'prettier', + ], + parser: '@typescript-eslint/parser', + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: __dirname, + sourceType: 'module', + }, + plugins: ['react', '@typescript-eslint'], + rules: { + 'react/require-default-props': 'off', + 'import/prefer-default-export': 'off', + 'import/no-default-export': 'error', + '@typescript-eslint/no-unused-vars': 'warn', + 'consistent-return': 'off', + 'import/no-extraneous-dependencies': 'off', + 'react/jsx-props-no-spreading': 'off', + 'react/button-has-type': 'off', + 'spaced-comment': 'off', + 'react/jsx-no-useless-fragment': 'off', + 'import/extensions': [ + 'error', + 'ignorePackages', + { + '': 'never', + js: 'never', + jsx: 'never', + ts: 'never', + tsx: 'never', + }, + ], + }, + ignorePatterns: ['react-app-env.d.ts'], +}; diff --git a/frontend/dev/gstd-racing-car-game/Dockerfile b/frontend/dev/gstd-racing-car-game/Dockerfile new file mode 100644 index 000000000..09bdfd97a --- /dev/null +++ b/frontend/dev/gstd-racing-car-game/Dockerfile @@ -0,0 +1,38 @@ +FROM node:18-alpine +MAINTAINER gear + +WORKDIR /frontend + +COPY /frontend/package.json . +COPY /frontend/yarn.lock . +COPY /frontend/.yarnrc.yml . +COPY /frontend/.yarn/releases .yarn/releases + +COPY ./frontend/apps/racing-car-game ./apps/racing-car-game +COPY ./frontend/packages ./packages + +RUN apk update + +RUN apk add xsel + +ARG REACT_APP_DNS_API_URL \ + REACT_APP_DNS_NAME \ + REACT_APP_NODE_ADDRESS \ + REACT_APP_GASLESS_BACKEND_ADDRESS \ + REACT_APP_SENTRY_DSN +ENV REACT_APP_DNS_API_URL=${REACT_APP_DNS_API_URL} \ + REACT_APP_DNS_NAME=${REACT_APP_DNS_NAME} \ + REACT_APP_NODE_ADDRESS=${REACT_APP_NODE_ADDRESS} \ + REACT_APP_GASLESS_BACKEND_ADDRESS=${REACT_APP_GASLESS_BACKEND_ADDRESS} \ + REACT_APP_SENTRY_DSN=${REACT_APP_SENTRY_DSN} \ + DISABLE_ESLINT_PLUGIN=true + +WORKDIR /frontend/apps/racing-car-game + +RUN yarn install + +RUN yarn build + +RUN npm install --global serve + +CMD ["serve", "-s", "/frontend/apps/racing-car-game/build"] diff --git a/frontend/dev/gstd-racing-car-game/README.md b/frontend/dev/gstd-racing-car-game/README.md new file mode 100644 index 000000000..854f7fe77 --- /dev/null +++ b/frontend/dev/gstd-racing-car-game/README.md @@ -0,0 +1,35 @@ +

+ + GEAR + +

+

+ +

+
+ +## Description + +React application of [Racing Cars Game](#https://wiki.gear-tech.io/docs/examples/Gaming/racingcars) based on [Rust smart-contract](#https://github.com/gear-foundation/dapps/tree/master/contracts/car-races). + +## Getting started + +### Install packages: + +```sh +yarn install +``` + +### Declare environment variables: + +Create `.env` file, `.env.example` will let you know what variables are expected. + +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. + +### Run the app: + +```sh +yarn run start +``` diff --git a/frontend/dev/gstd-racing-car-game/config-overrides.js b/frontend/dev/gstd-racing-car-game/config-overrides.js new file mode 100644 index 000000000..3ff8c47f9 --- /dev/null +++ b/frontend/dev/gstd-racing-car-game/config-overrides.js @@ -0,0 +1,26 @@ +const webpack = require('webpack'); +const path = require(`path`); + +const SRC = `src`; + +module.exports = (config) => { + config.plugins.push(new webpack.ProvidePlugin({ Buffer: ['buffer', 'Buffer'] })); + return { + ...config, + plugins: [ + ...config.plugins, + new webpack.ProvidePlugin({ + Buffer: ['buffer', 'Buffer'], + }), + ], + resolve: { + ...config.resolve, + alias: { + '@': path.resolve(__dirname, `${SRC}`), + }, + }, + devServer: { + port: 3000, + }, + }; +}; diff --git a/frontend/dev/gstd-racing-car-game/index.d.ts b/frontend/dev/gstd-racing-car-game/index.d.ts new file mode 100644 index 000000000..6efbd32d2 --- /dev/null +++ b/frontend/dev/gstd-racing-car-game/index.d.ts @@ -0,0 +1,7 @@ +export {}; + +declare global { + interface Window { + walletExtension: { isNovaWallet: boolean }; + } +}