DEV Community

Cover image for How to Build a Web3 Play-To-Earn Platform with Next.js, Typescript, and Solidity
Gospel Darlington
Gospel Darlington

Posted on

2

How to Build a Web3 Play-To-Earn Platform with Next.js, Typescript, and Solidity

What you will be building, see our git repo for the finished work and our live demo.

Play2EarnX

Play2EarnX

Introduction

Welcome to our comprehensive guide on "Building a Web3 Play-To-Earn Platform with Next.js, Typescript, and Solidity". In this tutorial, we'll build a decentralized Play-To-Earn platform that leverages the power of blockchain technology. You'll gain a clear understanding of the following:

  • Building dynamic interfaces with Next.js
  • Crafting Ethereum smart contracts with Solidity
  • Incorporating static type checking using TypeScript
  • Deploying and interacting with your smart contracts
  • Understanding the fundamentals of blockchain-based Play-To-Earn platforms

By the end of this guide, you'll have a functioning decentralized platform where users can participate in Play-To-Earn games, with all activities managed and secured by Ethereum smart contracts.

As an added incentive for participating in this tutorial, we're giving away a copy of our prestigious book on becoming an in-demand Solidity developer. This offer is free for the first 300 participants. For instructions on how to claim your copy, please watch the short video below.

Capturing Smart Contract Development

Prerequisites

You will need the following tools installed to build along with me:

  • Node.js
  • Yarn
  • Git Bash
  • MetaMask
  • Next.js
  • Solidity
  • Redux Toolkit
  • Tailwind CSS

To set up MetaMask for this tutorial, please watch the instructional video below:

Once you have successfully completed the setup, you are eligible to receive a free copy of our book. To claim your book, please fill out the form to submit your proof-of-work.

Watch the following instructional videos to receive up to 3-months of free premium courses on Dapp Mentors Academy, including:

Ready to take your Bitfinity skills to the next level? Dive into the intricacies of smart contract development on ICP as you build a Ticketing Management System in the upcoming module. Create, deploy, and manage your smart contracts on the Bitfinity network.

With that said, let’s jump into the tutorial and set up our project.

Setup

We'll start by cloning a prepared frontend repository and setting up the environment variables. Run the following commands:

git clone https://github.com/Daltonic/play2earnX
cd play2earnX
yarn install
git checkout 01_no_redux
Enter fullscreen mode Exit fullscreen mode

Next, create a .env file at the root of the project and include the following keys:

NEXT_PUBLIC_RPC_URL=http://127.0.0.1:8545
NEXT_PUBLIC_ALCHEMY_ID=<YOUR_ALCHEMY_PROJECT_ID>
NEXT_PUBLIC_PROJECT_ID=<WALLET_CONNECT_PROJECT_ID>
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=somereallysecretsecret
Enter fullscreen mode Exit fullscreen mode

Replace <YOUR_ALCHEMY_PROJECT_ID> and <WALLET_CONNECT_PROJECT_ID> with your respective project IDs.

YOUR_ALCHEMY_PROJECT_ID: Get Key Here
WALLET_CONNECT_PROJECT_ID: Get Key Here

Finally, run yarn dev to start the project.

Dummy Data

Our user interface is prepared to incorporate smart contracts, however, we still need to integrate Redux in order to facilitate the sharing of data.

Building the Redux Store

Store Structure

The above image represents the structure of our Redux store, it will be simple since we are not creating some overly complex project.

We'll set up Redux to manage our application's global state. Follow these steps:

  1. Create a store folder at the project root.
  2. Inside store, create two folders: actions and states.
  3. Inside states, create a globalStates.ts file.
import { GlobalState } from '@/utils/type.dt'
export const globalStates: GlobalState = {
games: [],
scores: [],
invitations: [],
game: null,
createModal: 'scale-0',
resultModal: 'scale-0',
inviteModal: 'scale-0',
}
view raw globalStates.ts hosted with ❤ by GitHub
  1. Inside actions, create a globalActions.ts file.
import { GameStruct, GlobalState, InvitationStruct, ScoreStruct } from '@/utils/type.dt'
import { PayloadAction } from '@reduxjs/toolkit'
export const globalActions = {
setGames: (state: GlobalState, action: PayloadAction<GameStruct[]>) => {
state.games = action.payload
},
setScores: (state: GlobalState, action: PayloadAction<ScoreStruct[]>) => {
state.scores = action.payload
},
setInvitations: (state: GlobalState, action: PayloadAction<InvitationStruct[]>) => {
state.invitations = action.payload
},
setGame: (state: GlobalState, action: PayloadAction<GameStruct | null>) => {
state.game = action.payload
},
setCreateModal: (state: GlobalState, action: PayloadAction<string>) => {
state.createModal = action.payload
},
setResultModal: (state: GlobalState, action: PayloadAction<string>) => {
state.resultModal = action.payload
},
setInviteModal: (state: GlobalState, action: PayloadAction<string>) => {
state.inviteModal = action.payload
},
}
  1. Create a globalSlices.ts file inside the store folder.
import { createSlice } from '@reduxjs/toolkit'
import { globalStates as GlobalStates } from './states/globalStates'
import { globalActions as GlobalActions } from './actions/globalActions'
export const globalSlices = createSlice({
name: 'global',
initialState: GlobalStates,
reducers: GlobalActions,
})
export const globalActions = globalSlices.actions
export default globalSlices.reducer
view raw globalSlices.ts hosted with ❤ by GitHub
  1. Create an index.ts file inside the store folder.
import { configureStore } from '@reduxjs/toolkit'
import globalSlices from './globalSlices'
export const store = configureStore({
reducer: {
globalStates: globalSlices,
},
})
view raw index.ts hosted with ❤ by GitHub
  1. Update the pages/_app.tsx file with the Redux provider.
import { ToastContainer } from 'react-toastify'
import '@/styles/global.css'
import 'react-toastify/dist/ReactToastify.css'
import '@rainbow-me/rainbowkit/styles.css'
import { useEffect, useState } from 'react'
import { Providers } from '@/services/provider'
import type { AppProps } from 'next/app'
import Header from '@/components/Header'
import { Provider } from 'react-redux'
import { store } from '@/store'
export default function App({ Component, pageProps }: AppProps) {
const [showChild, setShowChild] = useState<boolean>(false)
useEffect(() => {
setShowChild(true)
}, [])
if (!showChild || typeof window === 'undefined') {
return null
} else {
return (
<Providers pageProps={pageProps}>
<Provider store={store}>
<div className="bg-[#010922] min-h-screen">
<Header />
<Component {...pageProps} />
<ToastContainer
position="bottom-center"
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
theme="dark"
/>
</div>
</Provider>
</Providers>
)
}
}
view raw _app.tsx hosted with ❤ by GitHub

We have implemented Redux toolkit in our application and plan to revisit its usage when integrating the backend with the frontend.

Smart Contract Development

Next, we'll develop the smart contract for our platform:

  1. Create a contracts folder at the project root.
  2. Inside contracts, create a PlayToEarnX.sol file and add the contract code below.
//SPDX-License-Identifier:MIT
pragma solidity >=0.7.0 <0.9.0;
import '@openzeppelin/contracts/access/Ownable.sol';
import '@openzeppelin/contracts/utils/Counters.sol';
import '@openzeppelin/contracts/token/ERC20/ERC20.sol';
import '@openzeppelin/contracts/security/ReentrancyGuard.sol';
import '@openzeppelin/contracts/utils/math/SafeMath.sol';
contract PlayToEarnX is Ownable, ReentrancyGuard, ERC20 {
using Counters for Counters.Counter;
using SafeMath for uint256;
Counters.Counter private _totalGames;
Counters.Counter private _totalPlayers;
Counters.Counter private _totalInvitations;
struct GameStruct {
uint256 id;
string title;
string description;
address owner;
uint256 participants;
uint256 numberOfWinners;
uint256 acceptees;
uint256 stake;
uint256 startDate;
uint256 endDate;
uint256 timestamp;
bool deleted;
bool paidOut;
}
struct PlayerStruct {
uint id;
uint gameId;
address acount;
}
struct InvitationStruct {
uint256 id;
string title;
uint256 gameId;
address receiver;
address sender;
bool responded;
bool accepted;
uint256 stake;
uint256 timestamp;
}
struct ScoreStruct {
uint id;
uint gameId;
address player;
uint score;
uint prize;
bool played;
}
uint256 private totalBalance;
uint256 private servicePct;
mapping(uint => bool) gameExists;
mapping(uint256 => GameStruct) games;
mapping(uint256 => PlayerStruct[]) players;
mapping(uint256 => ScoreStruct[]) scores;
mapping(uint256 => mapping(address => bool)) isListed;
mapping(uint256 => mapping(address => bool)) isInvited;
mapping(uint256 => InvitationStruct[]) invitationsOf;
constructor(uint256 _pct) ERC20('Play To Earn', 'P2E') {
servicePct = _pct;
}
function createGame(
string memory title,
string memory description,
uint256 participants,
uint256 numberOfWinners,
uint256 startDate,
uint256 endDate
) public payable {
require(msg.value > 0 ether, 'Stake must be greater than zero');
require(participants > 1, 'Partiticpants must be greater than one');
require(bytes(title).length > 0, 'Title cannot be empty');
require(bytes(description).length > 0, 'Description cannot be empty');
require(startDate > 0, 'Start date must be greater than zero');
require(endDate > startDate, 'End date must be greater than start date');
require(numberOfWinners > 0, 'Number Of winners cannot be zero');
_totalGames.increment();
GameStruct memory game;
game.id = _totalGames.current();
game.title = title;
game.description = description;
game.participants = participants;
game.numberOfWinners = numberOfWinners;
game.startDate = startDate;
game.endDate = endDate;
game.stake = msg.value;
game.owner = msg.sender;
game.timestamp = currentTime();
ScoreStruct memory score;
score.id = scores[game.id].length;
score.gameId = game.id;
score.player = msg.sender;
scores[game.id].push(score);
games[game.id] = game;
gameExists[game.id] = true;
require(playedSaved(game.id), 'Failed to create player');
}
function deleteGame(uint256 gameId) public {
require(gameExists[gameId], 'Game not found');
require(games[gameId].owner == msg.sender, 'Unauthorized entity');
require(games[gameId].acceptees == 1, 'Participants already in game');
players[gameId].pop();
isListed[gameId][msg.sender] = false;
payTo(msg.sender, games[gameId].stake);
games[gameId].acceptees--;
games[gameId].deleted = true;
}
function playedSaved(uint256 gameId) internal returns (bool) {
_totalPlayers.increment();
PlayerStruct memory player;
player.id = _totalPlayers.current();
player.gameId = gameId;
player.acount = msg.sender;
players[gameId].push(player);
isListed[player.gameId][msg.sender] = true;
games[player.gameId].acceptees++;
totalBalance += msg.value;
return true;
}
function invitePlayer(address receiver, uint256 gameId) public {
require(gameExists[gameId], 'Game not found');
require(games[gameId].acceptees <= games[gameId].participants, 'Out of capacity');
require(!isListed[gameId][receiver], 'Player is already in this game');
_totalInvitations.increment();
InvitationStruct memory invitation;
invitation.id = _totalInvitations.current();
invitation.gameId = gameId;
invitation.title = games[gameId].title;
invitation.receiver = receiver;
invitation.sender = msg.sender;
invitation.timestamp = currentTime();
invitation.stake = games[gameId].stake;
isInvited[gameId][receiver] = true;
invitationsOf[gameId].push(invitation);
}
function acceptInvitation(uint256 gameId, uint256 index) public payable {
require(gameExists[gameId], 'Game not found');
require(msg.value >= games[gameId].stake, 'Insuffcient funds');
require(invitationsOf[gameId][index].receiver == msg.sender, 'Unauthorized entity');
require(playedSaved(gameId), 'Failed to create player');
invitationsOf[gameId][index].responded = true;
invitationsOf[gameId][index].accepted = true;
ScoreStruct memory score;
score.id = scores[gameId].length;
score.gameId = gameId;
score.player = msg.sender;
scores[gameId].push(score);
}
function rejectInvitation(uint256 gameId, uint256 index) public {
require(gameExists[gameId], 'Game not found');
require(invitationsOf[gameId][index].receiver == msg.sender, 'Unauthorized entity');
invitationsOf[gameId][index].responded = true;
}
function payout(uint256 gameId) public nonReentrant {
require(gameExists[gameId], 'Game does not exist');
require(currentTime() > games[gameId].endDate, 'Game still in session'); // disable on testing
require(!games[gameId].paidOut, 'Game already paid out');
GameStruct memory game = games[gameId];
uint256 totalStakes = game.stake.mul(game.acceptees);
uint256 platformFee = (totalStakes.mul(servicePct)).div(100);
uint256 creatorFee = platformFee.sub(platformFee.div(2));
uint256 gameRevenue = totalStakes.sub(platformFee).sub(creatorFee);
payTo(owner(), platformFee);
payTo(game.owner, creatorFee);
ScoreStruct[] memory Scores = new ScoreStruct[](scores[gameId].length);
Scores = sortScores(scores[gameId]);
for (uint256 i = 0; i < game.numberOfWinners; i++) {
uint256 payoutAmount = gameRevenue.div(game.numberOfWinners);
payTo(Scores[i].player, payoutAmount);
_mint(Scores[i].player, payoutAmount);
for (uint256 j = 0; j < scores[gameId].length; j++) {
if(Scores[i].player == scores[gameId][j].player) {
scores[gameId][j].prize = payoutAmount;
}
}
}
games[gameId].paidOut = true;
}
function sortScores(ScoreStruct[] memory scoreSheet) public pure returns (ScoreStruct[] memory) {
uint256 n = scoreSheet.length;
for (uint256 i = 0; i < n - 1; i++) {
for (uint256 j = 0; j < n - i - 1; j++) {
// Check if the players played before comparing their scores
if (scoreSheet[j].played && scoreSheet[j + 1].played) {
if (scoreSheet[j].score > scoreSheet[j + 1].score) {
// Swap the elements
ScoreStruct memory temp = scoreSheet[j];
scoreSheet[j] = scoreSheet[j + 1];
scoreSheet[j + 1] = temp;
}
} else if (!scoreSheet[j].played && scoreSheet[j + 1].played) {
// Sort players who didn't play below players who played
// Swap the elements
ScoreStruct memory temp = scoreSheet[j];
scoreSheet[j] = scoreSheet[j + 1];
scoreSheet[j + 1] = temp;
}
}
}
return scoreSheet;
}
function saveScore(uint256 gameId, uint256 index, uint256 score) public {
require(games[gameId].acceptees > games[gameId].numberOfWinners, 'Not enough players yet');
require(scores[gameId][index].gameId == gameId, 'Player not found');
require(!scores[gameId][index].played, 'Player already recorded');
require(scores[gameId][index].player == msg.sender, 'Unauthorized entity');
// require(
// currentTime() >= games[gameId].startDate && currentTime() < games[gameId].endDate,
// 'Game play must be in session'
// ); // disable on testing
scores[gameId][index].score = score;
scores[gameId][index].played = true;
scores[gameId][index].player = msg.sender;
}
function setFeePercent(uint256 _pct) public onlyOwner {
require(_pct > 0 && _pct <= 100, 'Fee percent must be in the range of (1 - 100)');
servicePct = _pct;
}
function getGames() public view returns (GameStruct[] memory Games) {
uint256 available;
for (uint256 i = 1; i <= _totalGames.current(); i++) {
if (!games[i].deleted && !games[i].paidOut) {
available++;
}
}
Games = new GameStruct[](available);
uint256 index;
for (uint256 i = 1; i <= _totalGames.current(); i++) {
if (!games[i].deleted && !games[i].paidOut) {
Games[index++] = games[i];
}
}
}
function getPaidOutGames() public view returns (GameStruct[] memory Games) {
uint256 available;
for (uint256 i = 1; i <= _totalGames.current(); i++) {
if (!games[i].deleted && games[i].paidOut) {
available++;
}
}
Games = new GameStruct[](available);
uint256 index;
for (uint256 i = 1; i <= _totalGames.current(); i++) {
if (!games[i].deleted && games[i].paidOut) {
Games[index++] = games[i];
}
}
}
function getMyGames() public view returns (GameStruct[] memory Games) {
uint256 available;
for (uint256 i = 1; i <= _totalGames.current(); i++) {
if (!games[i].deleted && games[i].owner == msg.sender) {
available++;
}
}
Games = new GameStruct[](available);
uint256 index;
for (uint256 i = 1; i <= _totalGames.current(); i++) {
if (!games[i].deleted && games[i].owner == msg.sender) {
Games[index++] = games[i];
}
}
}
function getGame(uint256 gameId) public view returns (GameStruct memory) {
return games[gameId];
}
function getInvitations(uint256 gameId) public view returns (InvitationStruct[] memory) {
return invitationsOf[gameId];
}
function getMyInvitations() public view returns (InvitationStruct[] memory Invitation) {
uint256 available;
for (uint256 i = 1; i <= _totalGames.current(); i++) {
if (isInvited[i][msg.sender]) {
available++;
}
}
Invitation = new InvitationStruct[](available);
uint256 index;
for (uint256 i = 1; i <= _totalGames.current(); i++) {
if (isInvited[i][msg.sender]) {
for (uint256 j = 0; j < invitationsOf[i].length; j++) {
Invitation[index] = invitationsOf[i][j];
Invitation[index].id = j;
}
index++;
}
}
}
function getScores(uint256 gameId) public view returns (ScoreStruct[] memory) {
return scores[gameId];
}
function payTo(address to, uint256 amount) internal {
(bool success, ) = payable(to).call{ value: amount }('');
require(success);
}
function currentTime() internal view returns (uint256) {
return (block.timestamp * 1000) + 1000;
}
}
view raw PlayToEarnX.sol hosted with ❤ by GitHub

The above smart contract is a Play-To-Earn (P2E) gaming platform, which is created with Solidity. The contract integrates features such as game creation, player invitations, score recording, and payout distribution.

Here's a breakdown of its logic, function by function:

  1. Constructor (**constructor(uint256 _pct) ERC20('Play To Earn', 'P2E')**): This function initializes the contract. It sets the service fee percentage and assigns initial token parameters.

  2. createGame Function: This function allows a user to create a new game, specifying the title, description, number of participants, number of winners, start date, and end date. It requires a stake greater than zero and validates all input data. A new game and score structure are created and stored in their respective mappings.

  3. deleteGame Function: This function allows the owner of a game to delete it, provided no participants have joined. It refunds the stake to the game owner and marks the game as deleted.

  4. playedSaved Function: This function is an internal function that creates a new player for a game. It increments the total players count, adds the new player to the players mapping, and increases the game's acceptees count.

  5. invitePlayer Function: This function allows a user to invite another player to a game. It checks if the game exists and if there is room for more players. An invitation is then created and added to the invitationsOf mapping.

  6. acceptInvitation Function: This function allows a user to accept an invitation to a game, provided they send enough funds to cover the game's stake. It creates a new player for the game and updates the invitation status.

  7. rejectInvitation Function: This function allows a user to reject an invitation.

  8. payout Function: This function handles the payout distribution at the end of a game. It checks if the game has ended and if it has not already been paid out. It calculates the total stakes, platform fee, creator fee, and game revenue. It pays out the platform fee to the contract owner, the creator fee to the game owner, and distributes the game revenue among the winners. It also mints new tokens for the winners.

  9. sortScores Function: This function sorts players based on their scores. It uses a simple bubble sort algorithm with a twist: it also takes into account whether a player has played the game or not. Players who have not played are sorted to the bottom.

  10. saveScore Function: This function allows a player to save their score for a game. It validates the game and player, then updates the player's score.

  11. setFeePercent Function: This function allows the contract owner to change the service fee percentage.

  12. getGames, getPaidOutGames, getMyGames Functions: These functions return lists of games based on different criteria (all games, games that have been paid out, and games owned by the caller).

  13. getGame Function: This function returns the details of a specific game.

  14. getInvitations Function: This function returns all invitations for a specific game.

  15. getMyInvitations Function: This function returns all invitations received by the caller.

  16. getScores Function: This function returns all scores for a specific game.

  17. payTo Function: This internal function sends funds to a specified address.

  18. currentTime Function: This internal function returns the current timestamp.

Contract Deployment and Seeding

Now, let's deploy our smart contract and populate it with some dummy data:

  1. Create a scripts folder at the project root.
  2. Inside scripts, create a deploy.js and a seed.js file and add the following codes.

Deploy Script

const { ethers } = require('hardhat')
const fs = require('fs')
async function deployContract() {
let contract
const taxPct = 5
try {
contract = await ethers.deployContract('PlayToEarnX', [taxPct])
await contract.waitForDeployment()
console.log('Contracts deployed successfully.')
return contract
} catch (error) {
console.error('Error deploying contracts:', error)
throw error
}
}
async function saveContractAddress(contract) {
try {
const address = JSON.stringify(
{
playToEarnXContract: contract.target,
},
null,
4
)
fs.writeFile('./contracts/contractAddress.json', address, 'utf8', (error) => {
if (error) {
console.error('Error saving contract address:', err)
} else {
console.log('Deployed contract address:', address)
}
})
} catch (error) {
console.error('Error saving contract address:', error)
throw error
}
}
async function main() {
let contract
try {
contract = await deployContract()
await saveContractAddress(contract)
console.log('Contract deployment completed successfully.')
} catch (error) {
console.error('Unhandled error:', error)
}
}
main().catch((error) => {
console.error('Unhandled error:', error)
process.exitCode = 1
})
view raw deploy.js hosted with ❤ by GitHub

Seed

const { faker } = require('@faker-js/faker')
const { ethers } = require('hardhat')
const fs = require('fs')
const toWei = (num) => ethers.parseEther(num.toString())
const dataCount = 1
const generateGameData = (count) => {
const games = []
for (let i = 0; i < count; i++) {
const game = {
id: i + 1,
title: faker.lorem.words(5),
description: faker.lorem.paragraph(),
owner: faker.string.hexadecimal({
length: { min: 42, max: 42 },
prefix: '0x',
}),
participants: 2,
winners: 1,
acceptees: faker.number.int({ min: 1, max: 2 }),
stake: faker.number.float({ min: 0.01, max: 0.1 }),
starts: faker.date.past().getTime(),
ends: faker.date.future().getTime(),
timestamp: faker.date.past().getTime(),
deleted: faker.datatype.boolean(),
paidOut: faker.datatype.boolean(),
}
games.push(game)
}
return games
}
const generateInvitations = async (count) => {
;[deployer, owner, player1, player2] = await ethers.getSigners()
const invitations = []
for (let i = 0; i < count; i++) {
const game = {
gameId: i + 1,
account: Math.random() < 0.5 ? player1 : player2,
stake: faker.number.float({ min: 0.01, max: 0.1 }),
responded: faker.datatype.boolean(),
accepted: faker.datatype.boolean(),
}
invitations.push(game)
}
return invitations
}
async function createGame(contract, game) {
const tx = await contract.createGame(
game.title,
game.description,
game.participants,
game.winners,
game.starts,
game.ends,
{ value: toWei(game.stake) }
)
await tx.wait()
}
async function sendInvitation(contract, player) {
const tx = await contract.invitePlayer(player.account, player.gameId)
await tx.wait()
}
async function getGames(contract) {
const result = await contract.getGames()
console.log('Games:', result)
}
async function getInvitations(contract, gameId) {
const result = await contract.getInvitations(gameId)
console.log('Invitations:', result)
}
async function getMyInvitations(contract) {
const result = await contract.getMyInvitations()
console.log('Invitations:', result)
}
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
async function main() {
let playToEarnXContract
try {
const contractAddresses = fs.readFileSync('./contracts/contractAddress.json', 'utf8')
const { playToEarnXContract: playToEarnXAddress } = JSON.parse(contractAddresses)
playToEarnXContract = await ethers.getContractAt('PlayToEarnX', playToEarnXAddress)
// Process #1
await Promise.all(
generateGameData(dataCount).map(async (game) => {
await createGame(playToEarnXContract, game)
})
)
await delay(2500) // Wait for 2.5 seconds
// Process #2
await Promise.all(
Array(dataCount)
.fill()
.map(async (_, i) => {
const randomCount = faker.number.int({ min: 1, max: 2 })
const invitations = await generateInvitations(randomCount)
await Promise.all(
invitations.map(async (player) => {
await sendInvitation(playToEarnXContract, player)
})
)
})
)
await delay(2500) // Wait for 2.5 seconds
// Process #3
await getGames(playToEarnXContract)
await getInvitations(playToEarnXContract, 1)
await getMyInvitations(playToEarnXContract)
} catch (error) {
console.error('Unhandled error:', error)
}
}
main().catch((error) => {
console.error('Unhandled error:', error)
process.exitCode = 1
})
view raw seed.js hosted with ❤ by GitHub
  1. Run the following commands to deploy the contract and seed it with data:

    yarn hardhat node # Run in terminal 1
    yarn hardhat run scripts/deploy.js # Run in terminal 2
    yarn hardhat run scripts/seed.js # Run in terminal 2

If you did that correctly, you should see a similar output like the one below:

Deployment

At this point we can start the integration of our smart contract to our frontend.

Frontend Integration

First, create a services folder at the project root, and inside it, create a blockchain.tsx file. This file will contain functions to interact with our smart contract.

import { ethers } from 'ethers'
import address from '@/contracts/contractAddress.json'
import p2eAbi from '@/artifacts/contracts/Play2EarnX.sol/PlayToEarnX.json'
import { GameParams, GameStruct, InvitationStruct, ScoreStruct } from '@/utils/type.dt'
import { globalActions } from '@/store/globalSlices'
import { store } from '@/store'
const toWei = (num: number) => ethers.parseEther(num.toString())
const fromWei = (num: number) => ethers.formatEther(num)
const { setInvitations, setGames, setScores } = globalActions
let ethereum: any
let tx: any
if (typeof window !== 'undefined') ethereum = (window as any).ethereum
const getEthereumContracts = async () => {
const accounts = await ethereum?.request?.({ method: 'eth_accounts' })
if (accounts?.length > 0) {
const provider = new ethers.BrowserProvider(ethereum)
const signer = await provider.getSigner()
const contracts = new ethers.Contract(address.playToEarnXContract, p2eAbi.abi, signer)
return contracts
} else {
const provider = new ethers.JsonRpcProvider(process.env.NEXT_PUBLIC_RPC_URL)
const wallet = ethers.Wallet.createRandom()
const signer = wallet.connect(provider)
const contracts = new ethers.Contract(address.playToEarnXContract, p2eAbi.abi, signer)
return contracts
}
}
const getOwner = async (): Promise<string> => {
const contract = await getEthereumContracts()
const owner = await contract.owner()
return owner
}
const getGames = async (): Promise<GameStruct[]> => {
const contract = await getEthereumContracts()
const games = await contract.getGames()
return structuredGames(games)
}
const getMyGames = async (): Promise<GameStruct[]> => {
const contract = await getEthereumContracts()
const games = await contract.getMyGames()
return structuredGames(games)
}
const getGame = async (gameId: number): Promise<GameStruct> => {
const contract = await getEthereumContracts()
const game = await contract.getGame(gameId)
return structuredGames([game])[0]
}
const getInvitations = async (gameId: number): Promise<InvitationStruct[]> => {
const contract = await getEthereumContracts()
const invitation = await contract.getInvitations(gameId)
return structuredInvitations(invitation)
}
const getMyInvitations = async (): Promise<InvitationStruct[]> => {
const contract = await getEthereumContracts()
const invitation = await contract.getMyInvitations()
return structuredInvitations(invitation)
}
const getScores = async (gameId: number): Promise<ScoreStruct[]> => {
const contract = await getEthereumContracts()
const scores = await contract.getScores(gameId)
return structuredScores(scores)
}
const createGame = async (game: GameParams): Promise<void> => {
if (!ethereum) {
reportError('Please install a browser provider')
return Promise.reject(new Error('Browser provider not installed'))
}
try {
const contract = await getEthereumContracts()
tx = await contract.createGame(
game.title,
game.description,
game.participants,
game.numberOfWinners,
game.startDate,
game.endDate,
{ value: toWei(Number(game.stake)) }
)
await tx.wait()
const games: GameStruct[] = await getGames()
store.dispatch(setGames(games))
return Promise.resolve(tx)
} catch (error) {
reportError(error)
return Promise.reject(error)
}
}
const deleteGame = async (gameId: number): Promise<void> => {
if (!ethereum) {
reportError('Please install a browser provider')
return Promise.reject(new Error('Browser provider not installed'))
}
try {
const contract = await getEthereumContracts()
tx = await contract.deleteGame(gameId)
await tx.wait()
const games: GameStruct[] = await getGames()
store.dispatch(setGames(games))
return Promise.resolve(tx)
} catch (error) {
reportError(error)
return Promise.reject(error)
}
}
const invitePlayer = async (receiver: string, gameId: number): Promise<void> => {
if (!ethereum) {
reportError('Please install a browser provider')
return Promise.reject(new Error('Browser provider not installed'))
}
try {
const contract = await getEthereumContracts()
tx = await contract.invitePlayer(receiver, gameId)
await tx.wait()
const invitations: InvitationStruct[] = await getInvitations(gameId)
store.dispatch(setInvitations(invitations))
return Promise.resolve(tx)
} catch (error) {
reportError(error)
return Promise.reject(error)
}
}
const saveScore = async (gameId: number, index: number, score: number): Promise<void> => {
if (!ethereum) {
reportError('Please install a browser provider')
return Promise.reject(new Error('Browser provider not installed'))
}
try {
const contract = await getEthereumContracts()
tx = await contract.saveScore(gameId, index, score)
await tx.wait()
const scores: ScoreStruct[] = await getScores(gameId)
store.dispatch(setScores(scores))
return Promise.resolve(tx)
} catch (error) {
reportError(error)
return Promise.reject(error)
}
}
const payout = async (gameId: number): Promise<void> => {
if (!ethereum) {
reportError('Please install a browser provider')
return Promise.reject(new Error('Browser provider not installed'))
}
try {
const contract = await getEthereumContracts()
tx = await contract.payout(gameId)
await tx.wait()
const scores: ScoreStruct[] = await getScores(gameId)
store.dispatch(setScores(scores))
return Promise.resolve(tx)
} catch (error) {
reportError(error)
return Promise.reject(error)
}
}
const respondToInvite = async (
accept: boolean,
invitation: InvitationStruct,
index: number
): Promise<void> => {
if (!ethereum) {
reportError('Please install a browser provider')
return Promise.reject(new Error('Browser provider not installed'))
}
try {
const contract = await getEthereumContracts()
if (accept) {
tx = await contract.acceptInvitation(invitation.gameId, index, {
value: toWei(invitation.stake),
})
} else {
tx = await contract.rejectInvitation(invitation.gameId, index)
}
await tx.wait()
const invitations: InvitationStruct[] = await getMyInvitations()
store.dispatch(setInvitations(invitations))
return Promise.resolve(tx)
} catch (error) {
reportError(error)
return Promise.reject(error)
}
}
const structuredGames = (games: GameStruct[]): GameStruct[] =>
games
.map((game) => ({
id: Number(game.id),
title: game.title,
participants: Number(game.participants),
numberOfWinners: Number(game.numberOfWinners),
acceptees: Number(game.acceptees),
stake: parseFloat(fromWei(game.stake)),
owner: game.owner,
description: game.description,
startDate: Number(game.startDate),
endDate: Number(game.endDate),
timestamp: Number(game.timestamp),
deleted: game.deleted,
paidOut: game.paidOut,
}))
.sort((a, b) => b.timestamp - a.timestamp)
const structuredInvitations = (invitations: InvitationStruct[]): InvitationStruct[] =>
invitations
.map((invitation) => ({
id: Number(invitation.id),
gameId: Number(invitation.gameId),
title: invitation.title,
sender: invitation.sender,
receiver: invitation.receiver,
stake: parseFloat(fromWei(invitation.stake)),
accepted: invitation.accepted,
responded: invitation.responded,
timestamp: Number(invitation.timestamp),
}))
.sort((a, b) => b.timestamp - a.timestamp)
const structuredScores = (scores: ScoreStruct[]): ScoreStruct[] =>
scores
.map((score) => ({
id: Number(score.id),
gameId: Number(score.gameId),
player: score.player,
prize: parseFloat(fromWei(score.prize)),
score: Number(score.score),
played: score.played,
}))
.sort((a, b) => a.score - b.score)
export {
getOwner,
getGames,
getMyGames,
getGame,
getScores,
getInvitations,
getMyInvitations,
respondToInvite,
createGame,
invitePlayer,
saveScore,
payout,
deleteGame,
}
view raw blockchain.tsx hosted with ❤ by GitHub

The above code is a service that interacts with our Play-To-Earn gaming smart contract on the chain. It uses the Ethers.js library to interact with the Ethereum blockchain for contract interaction.

Here's a detailed breakdown of its functions:

  1. getEthereumContracts Function: This function sets up a connection to the Ethereum blockchain and the smart contract. It uses the ethers library to create a provider (to interact with the Ethereum blockchain) and a signer (to sign transactions). Then, it creates a contract instance that can be used to interact with the smart contract.

  2. getOwner Function: This function retrieves the owner of the contract. It calls the owner function of the contract, which is defined in the OpenZeppelin's Ownable contract.

  3. getGames, getMyGames, getGame Functions: These functions retrieve information about the games. They call the corresponding functions in the contract and structure the returned data.

  4. getInvitations, getMyInvitations Functions: These functions retrieve information about the game invitations. They call the corresponding functions in the contract and structure the returned data.

  5. getScores Function: This function retrieves scores of a game. It calls the corresponding function in the contract and structures the returned data.

  6. createGame Function: This function creates a new game by calling the createGame function of the contract. It checks if the browser provider is installed and handles any errors that might occur.

  7. deleteGame Function: This function deletes a game by calling the deleteGame function of the contract. It checks if the browser provider is installed and handles any errors that might occur.

  8. invitePlayer Function: This function invites a player to a game by calling the invitePlayer function of the contract. It checks if the browser provider is installed and handles any errors that might occur.

  9. saveScore Function: This function saves a player's score by calling the saveScore function of the contract. It checks if the browser provider is installed and handles any errors that might occur.

  10. payout Function: This function handles payout distribution by calling the payout function of the contract. It checks if the browser provider is installed and handles any errors that might occur.

  11. respondToInvite Function: This function allows a user to respond to an invitation by calling either the acceptInvitation or rejectInvitation function of the contract, based on the user's response. It checks if the browser provider is installed and handles any errors that might occur.

  12. structuredGames, structuredInvitations, structuredScores Functions: These functions structure the data returned from the contract into a more manageable format. They convert the data types from the contract's format to JavaScript's format and sort the data.

Next, update the provider.tsx file inside services to include the bitfinity network using the following codes.

'use client'
import * as React from 'react'
import {
GetSiweMessageOptions,
RainbowKitSiweNextAuthProvider,
} from '@rainbow-me/rainbowkit-siwe-next-auth'
import { WagmiConfig, configureChains, createConfig } from 'wagmi'
import { Chain, RainbowKitProvider, connectorsForWallets, darkTheme } from '@rainbow-me/rainbowkit'
import {
metaMaskWallet,
trustWallet,
coinbaseWallet,
rainbowWallet,
} from '@rainbow-me/rainbowkit/wallets'
import { mainnet, hardhat } from 'wagmi/chains'
import { alchemyProvider } from 'wagmi/providers/alchemy'
import { publicProvider } from 'wagmi/providers/public'
import { Session } from 'next-auth'
import { SessionProvider } from 'next-auth/react'
const bitfinity: Chain = {
id: 355113,
name: 'Bitfinity',
network: 'bitfinity',
iconUrl: 'https://bitfinity.network/logo.png',
iconBackground: '#000000',
nativeCurrency: {
decimals: 18,
name: 'Bitfinity',
symbol: 'BFT',
},
rpcUrls: {
public: { http: ['https://testnet.bitfinity.network'] },
default: { http: ['https://testnet.bitfinity.network'] },
},
blockExplorers: {
default: { name: 'Bitfinity Block Explorer', url: 'https://explorer.bitfinity.network/' },
etherscan: { name: 'Bitfinity Block Explorer', url: 'https://explorer.bitfinity.network/' },
},
testnet: true,
}
const { chains, publicClient } = configureChains(
[mainnet, bitfinity, hardhat],
[alchemyProvider({ apiKey: process.env.NEXT_PUBLIC_ALCHEMY_ID as string }), publicProvider()]
)
const projectId = process.env.NEXT_PUBLIC_PROJECT_ID as string
const connectors = connectorsForWallets([
{
groupName: 'Recommended',
wallets: [
metaMaskWallet({ projectId, chains }),
trustWallet({ projectId, chains }),
coinbaseWallet({ appName: 'Coinbase', chains }),
rainbowWallet({ projectId, chains }),
],
},
])
const wagmiConfig = createConfig({
autoConnect: true,
connectors,
publicClient,
})
const demoAppInfo = {
appName: 'Dapp Funds dApp',
}
const getSiweMessageOptions: GetSiweMessageOptions = () => ({
statement: `
Once you're signed in, you'll be able to access all of our dApp's features.
Thank you for partnering with CrowdFunding!`,
})
export function Providers({
children,
pageProps,
}: {
children: React.ReactNode
pageProps: {
session: Session
}
}) {
const [mounted, setMounted] = React.useState(false)
React.useEffect(() => setMounted(true), [])
return (
<WagmiConfig config={wagmiConfig}>
<SessionProvider refetchInterval={0} session={pageProps.session}>
<RainbowKitSiweNextAuthProvider getSiweMessageOptions={getSiweMessageOptions}>
<RainbowKitProvider theme={darkTheme()} chains={chains} appInfo={demoAppInfo}>
{mounted && children}
</RainbowKitProvider>
</RainbowKitSiweNextAuthProvider>
</SessionProvider>
</WagmiConfig>
)
}
view raw provider.tsx hosted with ❤ by GitHub

Page Interacting with Smart Contract

Next, we'll link the functions in the blockchain service to their respective interfaces in the frontend:

No 1: Displaying Available Games
Update pages/index.tsx to get data from the getGames() function.

import CreateGame from '@/components/CreateGame'
import GameDetails from '@/components/GameDetails'
import GameList from '@/components/GameList'
import Hero from '@/components/Hero'
import { getGames } from '@/services/blockchain'
import { globalActions } from '@/store/globalSlices'
import { GameStruct, RootState } from '@/utils/type.dt'
import { NextPage } from 'next'
import Head from 'next/head'
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
const Page: NextPage<{ gamesData: GameStruct[] }> = ({ gamesData }) => {
const dispatch = useDispatch()
const { setGames } = globalActions
const { games } = useSelector((states: RootState) => states.globalStates)
useEffect(() => {
dispatch(setGames(gamesData))
}, [dispatch, setGames, gamesData])
return (
<div>
<Head>
<title>Play2Earn</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Hero />
{games.length > 1 && (
<>
<GameList games={games} />
<GameDetails />
</>
)}
<CreateGame />
</div>
)
}
export default Page
export const getServerSideProps = async () => {
const gamesData: GameStruct[] = await getGames()
return {
props: { gamesData: JSON.parse(JSON.stringify(gamesData)) },
}
}
view raw index.tsx hosted with ❤ by GitHub

No 2: Displaying User’s Games
Update pages/games.tsx to get data from the getMyGames() function.

import GameDetails from '@/components/GameDetails'
import GameList from '@/components/GameList'
import InviteModal from '@/components/InviteModal'
import { getMyGames } from '@/services/blockchain'
import { globalActions } from '@/store/globalSlices'
import { GameStruct, RootState } from '@/utils/type.dt'
import { NextPage } from 'next'
import Head from 'next/head'
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
const Page: NextPage = () => {
const dispatch = useDispatch()
const { setGames } = globalActions
const { games } = useSelector((states: RootState) => states.globalStates)
useEffect(() => {
const fetchGame = async () => {
const gamesData: GameStruct[] = await getMyGames()
dispatch(setGames(gamesData))
}
fetchGame()
}, [dispatch, setGames])
return (
<div>
<Head>
<title>Play2Earn | Game List</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<GameList games={games} />
<GameDetails />
<InviteModal />
</div>
)
}
export default Page
view raw games.tsx hosted with ❤ by GitHub

Notice how we combined useEffect() to dispatch the games into the store before rendering on screen.

No 3: Displaying User’s Invitations
Update pages/invitations.tsx to get data from the getMyInvitations() function.

import GameInvitations from '@/components/GameInvitations'
import InviteModal from '@/components/InviteModal'
import { getMyInvitations } from '@/services/blockchain'
import { globalActions } from '@/store/globalSlices'
import { InvitationStruct, RootState } from '@/utils/type.dt'
import { NextPage } from 'next'
import Head from 'next/head'
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
const Page: NextPage = () => {
const dispatch = useDispatch()
const { setGame, setInvitations } = globalActions
const { invitations } = useSelector((states: RootState) => states.globalStates)
useEffect(() => {
const fetchData = async () => {
const invitationsData: InvitationStruct[] = await getMyInvitations()
dispatch(setInvitations(invitationsData))
}
fetchData()
}, [dispatch, setGame, setInvitations])
return (
<div>
<Head>
<title>Play2Earn | My Invitation</title>
<link rel="icon" href="/favicon.ico" />
</Head>
{invitations && <GameInvitations invitations={invitations} label />}
<InviteModal />
</div>
)
}
export default Page
view raw invitations.tsx hosted with ❤ by GitHub

We also combined useEffect() to dispatch the invitations into the store before rendering on screen.

No 4: Displaying Game Invitations
Update pages/invitations/[id].tsx to use the getServerSideProps(), getGame(), and getInvitations() to retrieve a game invitations by specifying the game Id.

import GameInvitations from '@/components/GameInvitations'
import InviteModal from '@/components/InviteModal'
import { getGame, getInvitations } from '@/services/blockchain'
import { globalActions } from '@/store/globalSlices'
import { GameStruct, InvitationStruct, RootState } from '@/utils/type.dt'
import { GetServerSidePropsContext, NextPage } from 'next'
import Head from 'next/head'
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useAccount } from 'wagmi'
interface PageProps {
gameData: GameStruct
invitationsData: InvitationStruct[]
}
const Page: NextPage<PageProps> = ({ gameData, invitationsData }) => {
const { address } = useAccount()
const dispatch = useDispatch()
const { setGame, setInvitations, setInviteModal } = globalActions
const { game, invitations } = useSelector((states: RootState) => states.globalStates)
useEffect(() => {
dispatch(setGame(gameData))
dispatch(setInvitations(invitationsData))
}, [dispatch, setGame, gameData, setInvitations, invitationsData])
return (
<div>
<Head>
<title>Play2Earn | Game Invitation</title>
<link rel="icon" href="/favicon.ico" />
</Head>
{game && <GameInvitations game={game} invitations={invitations} />}
<div className="flex justify-center space-x-2">
{address === game?.owner && (
<button
onClick={() => dispatch(setInviteModal('scale-100'))}
className="bg-transparent border border-orange-700 hover:bg-orange-800
py-2 px-6 text-orange-700 hover:text-white rounded-full
transition duration-300 ease-in-out"
>
Invite Players
</button>
)}
</div>
<InviteModal />
</div>
)
}
export default Page
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { id } = context.query
const gameData: GameStruct = await getGame(Number(id))
const invitationsData: InvitationStruct[] = await getInvitations(Number(id))
return {
props: {
gameData: JSON.parse(JSON.stringify(gameData)),
invitationsData: JSON.parse(JSON.stringify(invitationsData)),
},
}
}
view raw [id].tsx hosted with ❤ by GitHub

No 5: Showcasing a Gameplay
Update pages/gameplay/[id].tsx to use the getServerSideProps(), getGame(), and getScores() to retrieve a game players by specifying the game Id.

import GameCard from '@/components/GameCard'
import { getGame, getScores, saveScore } from '@/services/blockchain'
import { GameCardStruct, GameStruct, ScoreStruct } from '@/utils/type.dt'
import { GetServerSidePropsContext, NextPage } from 'next'
import Head from 'next/head'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import {
GiAngelWings,
GiBeech,
GiBowArrow,
GiCrossedSwords,
GiShieldBounces,
GiSpartanHelmet,
} from 'react-icons/gi'
import { toast } from 'react-toastify'
import { useAccount } from 'wagmi'
const uniqueCardElements: GameCardStruct[] = [
{
id: 0,
name: 'Helmet',
icon: <GiSpartanHelmet size={100} />,
},
{
id: 1,
name: 'Beech',
icon: <GiBeech size={100} />,
},
{
id: 2,
name: 'Shield',
icon: <GiShieldBounces size={100} />,
},
{
id: 3,
name: 'Swords',
icon: <GiCrossedSwords size={100} />,
},
{
id: 4,
name: 'Wings',
icon: <GiAngelWings size={100} />,
},
{
id: 5,
name: 'Arrow',
icon: <GiBowArrow size={100} />,
},
]
const shuffleCards = (array: GameCardStruct[]) => {
const length = array.length
for (let i = length; i > 0; i--) {
const randomIndex = Math.floor(Math.random() * i)
const currentIndex = i - 1
const temp = array[currentIndex]
array[currentIndex] = array[randomIndex]
array[randomIndex] = temp
}
return array
}
interface PageComponents {
gameData: GameStruct
playerAddresses: string[]
scoresData: ScoreStruct[]
}
const Page: NextPage<PageComponents> = ({ gameData, playerAddresses, scoresData }) => {
const { address } = useAccount()
const [flipCount, setFlipCount] = useState<number>(0)
const [player, setPlayer] = useState<ScoreStruct | null>(null)
const [openCards, setOpenCards] = useState<GameCardStruct[]>([])
const [allCardsFlipped, setAllCardsFlipped] = useState<boolean>(false)
useEffect(() => {
setPlayer(scoresData.filter((player) => player.player === address)[0])
}, [])
const [cards, setCards] = useState<GameCardStruct[]>(
shuffleCards(
uniqueCardElements.concat(
uniqueCardElements.map((card, index) => ({
...card,
id: card.id + uniqueCardElements.length,
}))
)
)
)
const handleCardClick = (id: number) => {
setCards((prevCards) => {
const updatedCards = prevCards.map((card) =>
card.id === id ? { ...card, isFlipped: !card.isFlipped } : card
)
const allFlipped = updatedCards.every((card) => card.isFlipped)
setAllCardsFlipped(allFlipped)
return updatedCards
})
setFlipCount(flipCount + 1)
setOpenCards((prevOpenCards) => {
const newOpenCards = [...prevOpenCards, cards.find((card) => card.id === id)!]
if (newOpenCards.length === 2) {
if (newOpenCards[0].name === newOpenCards[1].name) {
// If the two cards are the same, clear the openCards array
return []
} else {
// If the two cards are not the same, flip them back after a delay
setTimeout(() => {
setCards((prevCards) =>
prevCards.map((card) =>
newOpenCards.find((openCard) => openCard.id === card.id)
? { ...card, isFlipped: false }
: card
)
)
}, 1000)
// Clear the openCards array
return []
}
}
// If there's only one card in openCards, keep it
return newOpenCards
})
}
const handleSubmit = async () => {
if (!address) return toast.warning('Connect wallet first!')
if (!player) return toast.warning('Player data not found')
await toast.promise(
new Promise<void>((resolve, reject) => {
saveScore(player.gameId, player.id, flipCount)
.then((tx) => {
console.log(tx)
resetGame()
resolve(tx)
})
.catch((error) => reject(error))
}),
{
pending: 'Approve transaction...',
success: 'Score saved successfully 👌',
error: 'Encountered error 🤯',
}
)
}
const resetGame = () => {
setCards(
shuffleCards(
uniqueCardElements.concat(
uniqueCardElements.map((card, index) => ({
...card,
id: card.id + uniqueCardElements.length,
}))
)
)
)
setOpenCards([])
setFlipCount(0)
setAllCardsFlipped(false)
}
return (
<div>
<Head>
<title>Play2Earn | {gameData?.title}</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<div className="min-h-screen flex flex-col justify-center items-center space-y-8">
<h4 className="text-2xl font-semibold text-blue-700">
We are keeping count of your flips, beware...
</h4>
<div className="grid grid-cols-4 gap-4">
{cards.map((card: GameCardStruct, i: number) => (
<GameCard
key={i}
card={card}
isDisabled={card.isFlipped || false}
onClick={() => handleCardClick(card.id)}
/>
))}
</div>
<div className="flex space-x-2">
<button
className="bg-transparent border border-blue-700 hover:bg-blue-800
py-2 px-6 text-blue-700 hover:text-white rounded-full
transition duration-300 ease-in-out"
onClick={resetGame}
>
Reset Game
</button>
{playerAddresses.includes(String(address)) && allCardsFlipped && (
<button
onClick={handleSubmit}
className="bg-transparent border border-green-700 hover:bg-green-800
py-2 px-6 text-green-700 hover:text-white rounded-full
transition duration-300 ease-in-out"
>
Submit Game
</button>
)}
<Link
href={'/results/' + gameData.id}
className="bg-transparent border border-blue-700 hover:bg-blue-800
py-2 px-6 text-blue-700 hover:text-white rounded-full
transition duration-300 ease-in-out"
>
Check Result
</Link>
</div>
</div>
</div>
)
}
export default Page
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { id } = context.query
const gameData: GameStruct = await getGame(Number(id))
const scoresData: ScoreStruct[] = await getScores(Number(id))
const playerAddresses: string[] = scoresData.map((player) => player.player)
return {
props: {
gameData: JSON.parse(JSON.stringify(gameData)),
scoresData: JSON.parse(JSON.stringify(scoresData)),
playerAddresses: JSON.parse(JSON.stringify(playerAddresses)),
},
}
}
view raw [id].tsx hosted with ❤ by GitHub

The script mentioned above is the core of the frontend code for this page. It contains the necessary functions and behaviors to create a memory game where users can compete against each other.

No 6: Displaying Game Result
Update pages/results/[id].tsx to use the getServerSideProps(), getGame(), and getScores() to retrieve a game score by specifying the game Id.

import GameResult from '@/components/GameResult'
import { getGame, getScores, payout } from '@/services/blockchain'
import { globalActions } from '@/store/globalSlices'
import { GameStruct, RootState, ScoreStruct } from '@/utils/type.dt'
import { GetServerSidePropsContext, NextPage } from 'next'
import Head from 'next/head'
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { toast } from 'react-toastify'
import { useAccount } from 'wagmi'
interface PageProps {
gameData: GameStruct
scoresData: ScoreStruct[]
}
const Page: NextPage<PageProps> = ({ gameData, scoresData }) => {
const dispatch = useDispatch()
const { address } = useAccount()
const { setGame, setScores } = globalActions
const { game, scores } = useSelector((states: RootState) => states.globalStates)
useEffect(() => {
dispatch(setGame(gameData))
dispatch(setScores(scoresData))
}, [dispatch, setGame, gameData, setScores, scoresData, scores])
const handlePayout = async () => {
if (!address) return toast.warning('Connect wallet first!')
if (!game) return toast.warning('Game data not found')
await toast.promise(
new Promise<void>((resolve, reject) => {
payout(game.id)
.then((tx) => {
console.log(tx)
resolve(tx)
})
.catch((error) => reject(error))
}),
{
pending: 'Approve transaction...',
success: 'Score saved successfully 👌',
error: 'Encountered error 🤯',
}
)
}
return (
<div>
<Head>
<title>Play2Earn | Game Result</title>
<link rel="icon" href="/favicon.ico" />
</Head>
{game && <GameResult game={game} scores={scores} />}
<div className="flex justify-center space-x-2">
<button
className="bg-transparent border border-orange-700 hover:bg-orange-800
py-2 px-6 text-orange-700 hover:text-white rounded-full
transition duration-300 ease-in-out"
onClick={handlePayout}
>
Payout
</button>
</div>
</div>
)
}
export default Page
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { id } = context.query
const gameData: GameStruct = await getGame(Number(id))
const scoresData: ScoreStruct[] = await getScores(Number(id))
return {
props: {
gameData: JSON.parse(JSON.stringify(gameData)),
scoresData: JSON.parse(JSON.stringify(scoresData)),
},
}
}
view raw [id].tsx hosted with ❤ by GitHub

It's worth mentioning that users can make a payout from this page after the game duration has ended.

Components with Smart Contract

Let's apply the same approach we used for the previous pages and update the following components to interact with the smart contract.

No 1: Creating New Games
Update components/CreateGame.tsx file to use the handleGameCreation() function to call the createGame() function for form submission.

import { createGame } from '@/services/blockchain'
import { globalActions } from '@/store/globalSlices'
import { GameParams, RootState } from '@/utils/type.dt'
import React, { ChangeEvent, FormEvent, useState } from 'react'
import { FaTimes } from 'react-icons/fa'
import { useDispatch, useSelector } from 'react-redux'
import { toast } from 'react-toastify'
const CreateGame: React.FC = () => {
const { createModal } = useSelector((states: RootState) => states.globalStates)
const dispatch = useDispatch()
const { setCreateModal } = globalActions
const [game, setGame] = useState<GameParams>({
title: '',
description: '',
participants: '',
numberOfWinners: '',
startDate: '',
endDate: '',
stake: '',
})
const handleChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target
setGame((prevState) => ({
...prevState,
[name]: value,
}))
}
const closeModal = () => {
dispatch(setCreateModal('scale-0'))
setGame({
title: '',
participants: '',
numberOfWinners: '',
startDate: '',
endDate: '',
description: '',
stake: '',
})
}
const handleGameCreation = async (e: FormEvent) => {
e.preventDefault()
game.startDate = new Date(game.startDate).getTime()
game.endDate = new Date(game.endDate).getTime()
await toast.promise(
new Promise(async (resolve, reject) => {
createGame(game)
.then((tx) => {
console.log(tx)
closeModal()
resolve(tx)
})
.catch((error) => reject(error))
}),
{
pending: 'Approve transaction...',
success: 'Game creation successful 👌',
error: 'Encountered error 🤯',
}
)
}
return (
<div
className={`fixed top-0 left-0 w-screen h-screen flex items-center justify-center
bg-black bg-opacity-50 transform z-50 transition-transform duration-300 ${createModal}`}
>
<div className="bg-[#010922] text-gray-300 shadow-md shadow-blue-900 rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
<div className="flex flex-col">
<div className="flex flex-row justify-between items-center">
<p className="font-semibold">Create Game</p>
<button onClick={closeModal} className="border-0 bg-transparent focus:outline-none">
<FaTimes />
</button>
</div>
<form
className="flex flex-col justify-center items-start rounded-xl mt-5 mb-5"
onSubmit={handleGameCreation}
>
<div className="flex flex-col sm:flex-row justify-between items-center w-full space-x-2 my-3">
<div className="w-full">
<label className="text-[12px]">Participants</label>
<div className="py-2 w-full border border-blue-900 rounded-full flex items-center px-4">
<input
placeholder="E.g 9"
type="number"
className="bg-transparent outline-none w-full placeholder-[#3D3857] text-sm border-none focus:outline-none focus:ring-0 py-0"
name="participants"
value={game.participants}
onChange={handleChange}
required
/>
</div>
</div>
<div className="w-full">
<label className="text-[12px]">Number of Winners</label>
<div className="py-2 w-full border border-blue-900 rounded-full flex items-center px-4">
<input
placeholder="E.g 2"
type="number"
min={1}
className="bg-transparent outline-none w-full placeholder-[#3D3857] text-sm border-none focus:outline-none focus:ring-0 py-0"
name="numberOfWinners"
value={game.numberOfWinners}
onChange={handleChange}
required
/>
</div>
</div>
<div className="w-full">
<label className="text-[12px]">Stake Amount</label>
<div className="py-2 w-full border border-blue-900 rounded-full flex items-center px-4">
<input
placeholder="E.g 2"
type="number"
className="bg-transparent outline-none w-full placeholder-[#3D3857] text-sm border-none focus:outline-none focus:ring-0 py-0"
name="stake"
value={game.stake}
onChange={handleChange}
step={0.0001}
min={0.0001}
required
/>
</div>
</div>
</div>
<label className="text-[12px]">Title</label>
<div className="py-2 w-full border border-blue-900 rounded-full flex items-center px-4 mb-3 mt-2">
<input
placeholder="Title"
className="bg-transparent outline-none w-full placeholder-[#3D3857] text-sm border-none focus:outline-none focus:ring-0 py-0"
name="title"
type="text"
value={game.title}
onChange={handleChange}
required
/>
</div>
<div className="flex flex-col sm:flex-row justify-between items-center w-full space-x-2 my-3">
<div className="w-full">
<label className="text-[12px]">Starts On</label>
<div className="py-2 w-full border border-blue-900 rounded-full flex items-center px-4 mb-3 mt-2">
<input
placeholder="Start Date"
className="bg-transparent outline-none w-full placeholder-[#3D3857] text-sm border-none focus:outline-none focus:ring-0 py-0"
name="startDate"
type="datetime-local"
value={game.startDate}
onChange={handleChange}
required
/>
</div>
</div>
<div className="w-full">
<label className="text-[12px]">Ends On</label>
<div className="py-2 w-full border border-blue-900 rounded-full flex items-center px-4 mb-3 mt-2">
<input
placeholder="End Date"
className="bg-transparent outline-none w-full placeholder-[#3D3857] text-sm border-none focus:outline-none focus:ring-0 py-0"
name="endDate"
type="datetime-local"
value={game.endDate}
onChange={handleChange}
required
/>
</div>
</div>
</div>
<label className="text-[12px]">Description</label>
<textarea
placeholder="What is this game about?"
className="h-[70px] w-full bg-transparent border border-blue-900 rounded-xl py-3 px-3
focus:outline-none focus:ring-0 resize-none
placeholder-[#3D3857] text-sm"
name="description"
value={game.description}
onChange={handleChange}
required
/>
<button
type="submit"
className="bg-transparent border border-blue-700 hover:bg-blue-800
py-2 px-6 text-blue-700 hover:text-white rounded-full
transition duration-300 ease-in-out mt-5"
>
Create Game
</button>
</form>
</div>
</div>
</div>
)
}
export default CreateGame
view raw CreateGame.tsx hosted with ❤ by GitHub

No 2: Handling Game Deletion
Update components/GameActions.tsx file to use the handleDelete() function to call the deleteGame() function.

import { Menu } from '@headlessui/react'
import { MdGames } from 'react-icons/md'
import { BsTrash3 } from 'react-icons/bs'
import { BiDotsVerticalRounded } from 'react-icons/bi'
import { FaUsers } from 'react-icons/fa6'
import { TbReportAnalytics } from 'react-icons/tb'
import React from 'react'
import { GameStruct } from '@/utils/type.dt'
import Link from 'next/link'
import { toast } from 'react-toastify'
import { deleteGame } from '@/services/blockchain'
import { useAccount } from 'wagmi'
const GameActions: React.FC<{ game: GameStruct }> = ({ game }) => {
const { address } = useAccount()
const handleDelete = async () => {
await toast.promise(
new Promise(async (resolve, reject) => {
deleteGame(game.id)
.then((tx) => {
console.log(tx)
resolve(tx)
})
.catch((error) => reject(error))
}),
{
pending: 'Approve transaction...',
success: 'Game deletion successful 👌',
error: 'Encountered error 🤯',
}
)
}
return (
<Menu as="div" className="inline-block text-left text-gray-300 relative">
<Menu.Button
className="inline-flex w-full justify-center
rounded-md bg-[#010922] bg-opacity-50 p-4 text-sm
font-medium hover:bg-opacity-30 focus:outline-none
focus-visible:ring-2 focus-visible:ring-white
focus-visible:ring-opacity-75"
>
<BiDotsVerticalRounded size={17} />
</Menu.Button>
<Menu.Items
className="absolute right-0 w-56 origin-top-right
divide-y divide-blue-700 rounded-md bg-[#010922] shadow-md
ing-1 ring-black ring-opacity-5 focus:outline-none shadow-blue-700 "
>
<Menu.Item>
{({ active }) => (
<Link
href={'/gameplay/' + game.id}
className={`flex justify-start items-center space-x-1 ${
active ? 'text-blue-700' : ''
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
>
<MdGames size={17} />
<span>Game Play</span>
</Link>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<Link
href={'/invitations/' + game.id}
className={`flex justify-start items-center space-x-1 ${
active ? 'text-orange-700' : ''
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
>
<FaUsers size={17} />
<span>Invitations</span>
</Link>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<Link
href={'/results/' + game.id}
className={`flex justify-start items-center space-x-1 ${
active ? 'text-green-700' : ''
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
>
<TbReportAnalytics size={17} />
<span>Result</span>
</Link>
)}
</Menu.Item>
{address == game.owner && (
<Menu.Item>
{({ active }) => (
<button
className={`flex justify-start items-center space-x-1 ${
active ? 'bg-red-700' : 'text-red-700'
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
onClick={handleDelete}
>
<BsTrash3 size={17} />
<span>Delete</span>
</button>
)}
</Menu.Item>
)}
</Menu.Items>
</Menu>
)
}
export default GameActions
view raw GameActions.tsx hosted with ❤ by GitHub

No 3: Displaying Game Details Modal
Update components/GameDetails.tsx file to use the closeModal() function to call the setResultModal() function.

import { globalActions } from '@/store/globalSlices'
import { timestampToDate, truncate } from '@/utils/helper'
import { RootState } from '@/utils/type.dt'
import React from 'react'
import { FaTimes } from 'react-icons/fa'
import { useDispatch, useSelector } from 'react-redux'
const GameDetails: React.FC = () => {
const dispatch = useDispatch()
const { setResultModal } = globalActions
const { game, resultModal } = useSelector((states: RootState) => states.globalStates)
const closeModal = () => {
dispatch(setResultModal('scale-0'))
}
return (
<div
className={`fixed top-0 left-0 w-screen h-screen flex items-center justify-center
bg-black bg-opacity-50 transform z-50 transition-transform duration-300 ${resultModal}`}
>
{game && (
<div className="bg-[#010922] text-gray-500 shadow-md shadow-blue-900 rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
<div className="flex flex-col">
<div className="flex flex-row justify-between items-center text-gray-300">
<p className="font-semibold capitalize">{game.title} (Instructions)</p>
<button onClick={closeModal} className="border-0 bg-transparent focus:outline-none">
<FaTimes />
</button>
</div>
<div className="flex flex-col justify-center items-start rounded-xl mt-5 space-y-4">
<ul className="list-disc list-inside text-md">
<li>
Host: {truncate({ text: game.owner, startChars: 4, endChars: 4, maxLength: 11 })}
</li>
<li>Participants: {game.participants}</li>
<li>Acceptees: {game.acceptees}</li>
<li>Rewards: {(game.stake * game.acceptees).toFixed(2)} ETH</li>
<li>Number of Winners: {game.numberOfWinners}</li>
<li>
Schedule: {timestampToDate(game.startDate)} - {timestampToDate(game.endDate)}
</li>
<li>
Payout Status:{' '}
{!game.paidOut ? (
'Yet to be paid out'
) : (
<span className="text-green-600 font-medium">Paid out</span>
)}
</li>
</ul>
<p>{game.description}</p>
</div>
</div>
</div>
)}
</div>
)
}
export default GameDetails
view raw GameDetails.tsx hosted with ❤ by GitHub

No 4: Inviting Players
Update components/GameInvitation.tsx file to use the handleResponse() function to call the respondToInvite() function.

import React from 'react'
import { GameStruct, InvitationStruct } from '@/utils/type.dt'
import { truncate } from '@/utils/helper'
import Identicon from 'react-identicons'
import { toast } from 'react-toastify'
import { useAccount } from 'wagmi'
import { respondToInvite } from '@/services/blockchain'
import Link from 'next/link'
interface ComponentProps {
game?: GameStruct
invitations: InvitationStruct[]
label?: boolean
}
const GameInvitations: React.FC<ComponentProps> = ({ invitations, game, label }) => {
const { address } = useAccount()
const handleResponse = async (accept: boolean, invitation: InvitationStruct, index: number) => {
if (!address) return toast.warning('Connect wallet first!')
index = label ? invitation.id : index
await toast.promise(
new Promise<void>((resolve, reject) => {
respondToInvite(accept, invitation, index)
.then((tx) => {
console.log(tx)
resolve(tx)
})
.catch((error) => reject(error))
}),
{
pending: 'Approve transaction...',
success: 'Responded successfully successfully 👌',
error: 'Encountered error 🤯',
}
)
}
return (
<div className="flex flex-col items-center justify-center pt-32 pb-5 text-gray-500">
<h1 className="text-4xl text-gray-300 capitalize font-bold mb-10">
{game ? `${game.title} Invitations` : 'Invitations'}
</h1>
<div className="w-full max-w-2xl mx-auto">
{invitations.map((invitation, index) => (
<div
key={index}
className="flex items-center shadow-md my-2
border border-blue-900 p-6 rounded-lg"
>
<div className="flex-grow">
<div className="flex justify-start items-center space-x-2">
<Identicon
className="rounded-full overflow-hidden shadow-md"
size={30}
string={label ? invitation.sender : invitation.receiver}
/>
<div>
{label ? (
<Link
href={'/gameplay/' + invitation.gameId}
className="font-medium capitalize"
>
{invitation.title}
</Link>
) : (
<Link
href={'/gameplay/' + invitation.gameId}
className="font-medium capitalize"
>
{truncate({
text: label ? invitation.sender : invitation.receiver,
startChars: 4,
endChars: 4,
maxLength: 11,
})}
</Link>
)}
<p>{invitation.stake.toFixed(2)} ETH</p>
</div>
</div>
</div>
{invitation.responded && (
<div
className={`font-bold text-lg ${
invitation.accepted ? 'text-green-700' : 'text-red-700'
}`}
>
{invitation.accepted ? 'Accepted' : 'Rejected'}
</div>
)}
{label && !invitation.responded && (
<div className="flex space-x-2">
<button
className="bg-transparent border border-blue-700 hover:bg-blue-800
py-2 px-6 text-blue-700 hover:text-gray-300 rounded-full
transition duration-300 ease-in-out"
onClick={() => handleResponse(true, invitation, index)}
>
Accept
</button>
<button
className="bg-transparent border border-red-700 hover:bg-red-800
py-2 px-6 text-red-700 hover:text-gray-300 rounded-full
transition duration-300 ease-in-out"
onClick={() => handleResponse(false, invitation, index)}
>
Reject
</button>
</div>
)}
</div>
))}
</div>
</div>
)
}
export default GameInvitations

No 5: Launching Game Details Modal
Update components/GameList.tsx file to use the openModal() function to call the setResultModal() function.

import React from 'react'
import { GameStruct } from '@/utils/type.dt'
import { formatDate, truncate } from '@/utils/helper'
import { useDispatch } from 'react-redux'
import { globalActions } from '@/store/globalSlices'
import GameActions from './GameActions'
const GameList: React.FC<{ games: GameStruct[] }> = ({ games }) => {
const dispatch = useDispatch()
const { setGame, setResultModal } = globalActions
const openModal = (game: GameStruct) => {
dispatch(setGame(game))
dispatch(setResultModal('scale-100'))
}
return (
<div className="lg:w-2/3 w-full mx-auto my-10 text-gray-300">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{games.length < 1 && <div className="text-lg font-semibold">No games yet</div>}
{games.map((game: GameStruct, i: number) => (
<div key={i} className="border border-blue-900 p-6 rounded-lg">
<div className="flex justify-between items-center">
<h3
onClick={() => openModal(game)}
className="text-lg font-semibold mb-2 capitalize cursor-pointer"
>
{game.title}
</h3>
<GameActions game={game} />
</div>
<p className="text-gray-500 mb-2">
{truncate({
text: game.description,
startChars: 100,
endChars: 0,
maxLength: 103,
})}
</p>
<p className="text-sm">Starts {formatDate(game.startDate)}</p>
<p className="text-blue-700 mt-2">
{truncate({
text: game.owner,
startChars: 4,
endChars: 4,
maxLength: 11,
})}
</p>
</div>
))}
</div>
</div>
)
}
export default GameList
view raw GameList.tsx hosted with ❤ by GitHub

Please note that this setResultModal() function actually launches the game details modal, that’s a little oversight in function name.

No 6: Launching the Create Game Modal
Update components/Hero.tsx file to dispatch the setCreateModal() function, this will help us launch the create game form modal.

import { globalActions } from '@/store/globalSlices'
import Link from 'next/link'
import React from 'react'
import { useDispatch } from 'react-redux'
const Hero: React.FC = () => {
const dispatch = useDispatch()
const { setCreateModal } = globalActions
return (
<section className="py-32">
<main
className="lg:w-2/3 w-full mx-auto flex flex-col justify-center
items-center h-full text-gray-300"
>
<h2 className="text-4xl">
Welcome to Play2<span className="text-blue-700">Earn</span>, Where Fun Meets Fortune!
</h2>
<p className="text-center my-4 ">
Get Ready to Unleash Your Inner Hero and Make Gaming Pay!
</p>
<div className="flex space-x-3 my-3">
<button
className="bg-blue-700 border-[1px] py-3 px-5 duration-200
transition-all hover:bg-blue-600"
onClick={() => dispatch(setCreateModal('scale-100'))}
>
Create Game
</button>
<Link
href="/games"
className="border-[1px] border-blue-700 text-blue-700 py-3 px-5
duration-200 transition-all hover:bg-blue-700 hover:text-gray-300"
>
My Games
</Link>
</div>
</main>
</section>
)
}
export default Hero
view raw Hero.tsx hosted with ❤ by GitHub

No 7: Launching the Invite Modal
Lastly, update components/InviteModal.tsx file to use the sendInvitation() function to call the invitePlayer() function.

import { invitePlayer } from '@/services/blockchain'
import { globalActions } from '@/store/globalSlices'
import { RootState } from '@/utils/type.dt'
import React, { FormEvent, useState } from 'react'
import { FaTimes } from 'react-icons/fa'
import { useDispatch, useSelector } from 'react-redux'
import { toast } from 'react-toastify'
const InviteModal: React.FC = () => {
const [player, setPlayer] = useState('')
const { game, inviteModal } = useSelector((states: RootState) => states.globalStates)
const dispatch = useDispatch()
const { setInviteModal } = globalActions
const sendInvitation = async (e: FormEvent) => {
e.preventDefault()
if (!game) return toast.warning('Game data not found')
await toast.promise(
new Promise(async (resolve, reject) => {
invitePlayer(player, game?.id)
.then((tx) => {
console.log(tx)
closeModal()
resolve(tx)
})
.catch((error) => reject(error))
}),
{
pending: 'Approve transaction...',
success: 'Invitation sent successful 👌',
error: 'Encountered error 🤯',
}
)
}
const closeModal = () => {
dispatch(setInviteModal('scale-0'))
}
return (
<div
className={`fixed top-0 left-0 w-screen h-screen flex items-center justify-center
bg-black bg-opacity-50 transform z-50 transition-transform duration-300 ${inviteModal}`}
>
<div className="bg-[#010922] text-gray-300 shadow-md shadow-blue-900 rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
<div className="flex flex-col">
<div className="flex flex-row justify-between items-center">
<p className="font-semibold">Invite Player</p>
<button onClick={closeModal} className="border-0 bg-transparent focus:outline-none">
<FaTimes />
</button>
</div>
<form
onSubmit={sendInvitation}
className="flex flex-col justify-center items-start rounded-xl mt-5 mb-5"
>
<div className="py-2 w-full border border-[#212D4A] rounded-full flex items-center px-4 mb-3 mt-2">
<input
className="bg-transparent outline-none w-full placeholder-[#3D3857] text-sm
border-none focus:outline-none focus:ring-0 py-0"
name="player"
type="text"
value={player}
onChange={(e) => setPlayer(e.target.value)}
minLength={42}
maxLength={42}
pattern="[A-Za-z0-9]+"
placeholder="Player ETH Account"
required
/>
</div>
<button
type="submit"
className="bg-transparent border border-blue-700 hover:bg-blue-800
py-2 px-6 text-blue-700 hover:text-white rounded-full
transition duration-300 ease-in-out mt-5"
>
Send Invite
</button>
</form>
</div>
</div>
</div>
)
}
export default InviteModal
view raw InviteModal.tsx hosted with ❤ by GitHub

The project is now complete as all components and pages are connected to the smart contract through the implementation of these updates.

If your Next.js server was offline, you can bring it back up by executing the command **yarn dev**.

For further learning, we recommends watch the full video of this build on our YouTube channel and visiting our website for additional resources.

Conclusion

In this tutorial, we've successfully built a decentralized Play-To-Earn platform using Next.js, TypeScript, and Solidity. We've established the development environment, constructed the Redux store, and deployed our smart contract to our local chain.

By merging the smart contract with the frontend, we've ensured a seamless gaming experience. This guide has equipped you with the skills to create dynamic interfaces, craft Ethereum smart contracts, manage shared data with Redux, and interact with smart contracts from the frontend. Now, you're ready to create your own Web3 Play-To-Earn platform. Happy coding!

About Author

I am a web3 developer and the founder of Dapp Mentors, a company that helps businesses and individuals build and launch decentralized applications. I have over 7 years of experience in the software industry, and I am passionate about using blockchain technology to create new and innovative applications. I run a YouTube channel called Dapp Mentors where I share tutorials and tips on web3 development, and I regularly post articles online about the latest trends in the blockchain space.

Stay connected with us, join communities on
Discord: Join
X-Twitter: Follow
LinkedIn: Connect
GitHub: Explore
Website: Visit

Image of Docusign

Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more

Top comments (0)