DEV Community

Cover image for How to Build a Web3 Play-To-Earn Money Dapp with React, Solidity, and CometChat
Gospel Darlington
Gospel Darlington

Posted on

1 2

How to Build a Web3 Play-To-Earn Money Dapp with React, Solidity, and CometChat

What you will be building, see the live demo at sepolia test net and the git repo.

Game Play

The Chat Interface

Are you captivated by the limitless potential of blockchain technology? Does the blend of innovation and financial opportunities intrigue you? If you're a blockchain enthusiast eager to delve into the captivating world of play-to-earn money applications, you're exactly where you need to be.

In this comprehensive tutorial, consider us your companions on an exhilarating journey to construct an advanced Web3 Play-To-Earn Money DApp. Just as those before you have ventured into the realm of blockchain, you're about to explore a new horizon. By harnessing the combined capabilities of React, Solidity, and CometChat, we'll guide you through every step of the way.

With React as your toolkit, you'll craft a dynamic front-end interface that beckons users into an immersive experience. Solidity, the heart of Ethereum's smart contracts, will empower you to infuse financial logic into each interaction. And finally, CometChat, our communication ally, will bridge connections in real-time, fostering seamless interaction among users.

In this tutorial, you'll gain mastery over:

  • Dynamic React Interfaces
  • Solidity Smart Contracts
  • Strategies for Profit
  • Real-time CometChat Integration
  • Decentralized Insights

Whether you're a seasoned developer or a newcomer to web3, this tutorial equips you with the skills to craft your own Play-To-Earn DApp. Together, let's embark on this exhilarating journey, merging React, Solidity, and CometChat – the trio that shapes the world of Web3 play-to-earn awaits your innovation! 🌟

Prerequisites

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

  • Node.js
  • Yarn
  • MetaMask
  • React
  • Solidity
  • CometChat SDK
  • Tailwind CSS

I recommend watching the video below to learn how to set up your MetaMask for this project

Installing Dependencies

Clone the starter kit and open it in VS Code using the command below:

git clone https://github.com/Daltonic/tailwind_ethers_starter_kit playToEarn
cd playToEarn
Enter fullscreen mode Exit fullscreen mode

Next, update the package.json with the snippet below.

{
"name": "demo",
"private": true,
"version": "0.0.0",
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
},
"dependencies": {
"@cometchat-pro/chat": "3.0.13",
"@headlessui/react": "^1.7.16",
"@nomiclabs/hardhat-ethers": "^2.1.0",
"@nomiclabs/hardhat-waffle": "^2.0.3",
"emojtcha-react": "^1.0.6",
"ethereum-waffle": "^3.4.4",
"ethers": "^5.6.9",
"hardhat": "^2.10.1",
"moment": "^2.29.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hooks-global-state": "^1.0.2",
"react-icons": "^4.3.1",
"react-identicons": "^1.2.5",
"react-moment": "^1.1.2",
"react-router-dom": "6",
"react-scripts": "5.0.0",
"react-toastify": "^9.1.3",
"web-vitals": "^2.1.4"
},
"devDependencies": {
"@openzeppelin/contracts": "^4.5.0",
"@tailwindcss/forms": "0.4.0",
"assert": "^2.0.0",
"autoprefixer": "10.4.2",
"babel-polyfill": "^6.26.0",
"babel-preset-env": "^1.7.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-stage-2": "^6.24.1",
"babel-preset-stage-3": "^6.24.1",
"babel-register": "^6.26.0",
"buffer": "^6.0.3",
"chai": "^4.3.6",
"chai-as-promised": "^7.1.1",
"crypto-browserify": "^3.12.0",
"dotenv": "^16.0.0",
"https-browserify": "^1.0.0",
"mnemonics": "^1.1.3",
"os-browserify": "^0.3.0",
"postcss": "8.4.5",
"process": "^0.11.10",
"react-app-rewired": "^2.1.11",
"stream-browserify": "^3.0.0",
"stream-http": "^3.2.0",
"tailwindcss": "3.0.18",
"url": "^0.11.0"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
view raw package.json hosted with ❤ by GitHub

Please run the command yarn install in your terminal to install the dependencies for this project.

Configuring CometChat SDK

To configure the CometChat SDK, please follow the steps provided below. Once completed, make sure to save the generated keys as environment variables for future use.

STEP 1:
Head to CometChat Dashboard and create an account.

Register a new CometChat account if you do not have one

STEP 2:
Log in to the CometChat dashboard, only after registering.

Log in to the CometChat Dashboard with your created account

STEP 3:
From the dashboard, add a new app called Play-To-Earn.

Create a new CometChat app - Step 1

Create a new CometChat app - Step 2

STEP 4:
Select the app you just created from the list.

Select your created app

From the Quick Start copy the APP_ID, REGION, and AUTH_KEY, to your .env file. See the image and code snippet.

Copy the the APP_ID, REGION, and AUTH_KEY

Replace the REACT_COMET_CHAT placeholder keys with their appropriate values.

REACT_APP_COMETCHAT_APP_ID=****************
REACT_APP_COMETCHAT_AUTH_KEY=******************************
REACT_APP_COMETCHAT_REGION=**
Enter fullscreen mode Exit fullscreen mode

The .env file should be created at the root of your project.

Configuring the Hardhat script

Navigate to the root directory of the project and open the "hardhat.config.js" file. Replace the existing content of the file with the provided settings.

require('@nomiclabs/hardhat-waffle')
require('dotenv').config()
module.exports = {
defaultNetwork: 'localhost',
networks: {
localhost: {
url: 'http://127.0.0.1:8545',
},
},
solidity: {
version: '0.8.11',
settings: {
optimizer: {
enabled: true,
runs: 200,
},
},
},
paths: {
sources: './src/contracts',
artifacts: './src/abis',
},
mocha: {
timeout: 40000,
},
}

This code configures Hardhat for your project. It includes importing necessary plugins, setting up networks (with localhost as the default), specifying the Solidity compiler version, defining paths for contracts and artifacts, and setting a timeout for Mocha tests.

The Smart Contract File

The following steps will guide you through the process of creating the smart contract file for this project:

  1. Create a new folder named contracts inside the src folder.
  2. Create a new file named PlayToEarn.sol inside the contracts folder.
  3. Copy the provided codes below and paste it into their respective files and save.

By following these steps, you will have successfully set up the necessary directory structure and created the PlayToEarn.sol file, which will serve as the foundation for implementing the logic of the smart contract.

//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/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
contract PlayToEarn is Ownable, ReentrancyGuard {
using Counters for Counters.Counter;
using SafeMath for uint256;
Counters.Counter private totalGame;
Counters.Counter private totalPlayers;
struct GameStruct {
uint id;
string title;
string description;
address owner;
uint participants;
uint numberOfWinners;
uint challenges;
uint plays;
uint acceptees;
uint stake;
uint startDate;
uint endDate;
uint timestamp;
bool deleted;
bool paidOut;
}
struct PlayerStruct {
uint id;
uint gameId;
address player;
}
struct InvitationStruct {
uint gameId;
address account;
bool responded;
bool accepted;
string title;
uint stake;
}
struct PlayerScoreSheetStruct {
uint gameId;
address player;
uint score;
bool played;
}
uint private totalBalance;
uint serviceFee = 5;
mapping(uint => GameStruct) games;
mapping(uint => PlayerStruct) players;
mapping(address => mapping(uint => InvitationStruct)) invitationsOf;
mapping(uint => mapping(address => bool)) isListed;
mapping(uint => bool) gameExists;
mapping(uint => bool) playerExists;
mapping(uint => mapping(address => bool)) invitationExists;
mapping(uint => mapping(address => PlayerScoreSheetStruct)) scores;
mapping(uint => bool) gameHasPlayers;
modifier onlyGameOwner(uint gameId) {
require(gameExists[gameId], "Game does not exist!");
require(msg.sender == games[gameId].owner, "Unauthorized entity");
_;
}
function createGame(
string memory title,
string memory description,
uint participants,
uint numberOfWinners,
uint challenges,
uint startDate,
uint endDate
) public payable {
require(msg.value > 0 ether, "Stake funds is required");
require(participants > 1, "Partiticpants must be greater than 1");
require(challenges >= 5, "Challenges must not be less than 5");
require(bytes(title).length > 0, "Title is required!");
require(bytes(description).length > 0, "Description is required!");
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 required!");
totalGame.increment();
uint gameId = totalGame.current();
bool isCreated = _saveGame(
gameId,
title,
description,
participants,
numberOfWinners,
challenges,
startDate,
endDate
);
require(isCreated, "Game creation failed");
isCreated = _savePlayer(gameId);
require(isCreated, "Player creation failed");
}
function getGames() public view returns (GameStruct[] memory ActiveGames) {
uint available;
for (uint256 i = 1; i <= totalGame.current(); i++) {
if (!games[i].deleted && !games[i].paidOut) {
available++;
}
}
ActiveGames = new GameStruct[](available);
uint index;
for (uint256 i = 1; i <= totalGame.current(); i++) {
if (!games[i].deleted && !games[i].paidOut) {
ActiveGames[index++] = games[i];
}
}
}
function getGame(uint id) public view returns (GameStruct memory) {
return games[id];
}
function invitePlayer(address playerAccount, uint gameId) public {
require(gameExists[gameId], "Game does not exist");
require(
games[gameId].acceptees <= games[gameId].participants,
"Out of capacity"
);
require(
!isListed[gameId][playerAccount],
"Player is already in this game"
);
invitationsOf[playerAccount][gameId] = InvitationStruct({
gameId: gameId,
account: playerAccount,
responded: false,
accepted: false,
title: games[gameId].title,
stake: games[gameId].stake
});
}
function acceptInvitation(uint gameId) public payable {
require(gameExists[gameId], "Game does not exist");
require(
msg.value >= games[gameId].stake,
"Insuffcient funds for stakes"
);
require(
invitationsOf[msg.sender][gameId].account == msg.sender,
"Unauthorized entity"
);
require(
!invitationsOf[msg.sender][gameId].responded,
"Previouly responded"
);
bool isCreated = _savePlayer(gameId);
require(isCreated, "Player creation failed");
invitationsOf[msg.sender][gameId].responded = true;
invitationsOf[msg.sender][gameId].accepted = true;
scores[gameId][msg.sender].player = msg.sender;
}
function rejectInvitation(uint gameId) public {
require(gameExists[gameId], "Game does not exist");
require(
invitationsOf[msg.sender][gameId].account == msg.sender,
"You are not invited to this game"
);
require(
!invitationsOf[msg.sender][gameId].responded,
"Invitation is already rejected"
);
invitationsOf[msg.sender][gameId].responded = true;
}
function getInvitations()
public
view
returns (InvitationStruct[] memory Invitations)
{
uint totalInvitations;
for (uint i = 1; i <= totalGame.current(); i++) {
if (invitationsOf[msg.sender][i].account == msg.sender)
totalInvitations++;
}
Invitations = new InvitationStruct[](totalInvitations);
uint index;
for (uint i = 1; i <= totalGame.current(); i++) {
if (invitationsOf[msg.sender][i].account == msg.sender) {
Invitations[index++] = invitationsOf[msg.sender][i];
}
}
}
function recordScore(uint gameId, uint score) public {
require(
games[gameId].numberOfWinners + 1 == games[gameId].acceptees,
"Not enough players yet"
);
require(!scores[gameId][msg.sender].played, "Player already recorded");
require(
currentTime() >= games[gameId].startDate &&
currentTime() < games[gameId].endDate,
"Game play must be in session"
);
games[gameId].plays++;
scores[gameId][msg.sender].score = score;
scores[gameId][msg.sender].played = true;
scores[gameId][msg.sender].player = msg.sender;
}
function getScores(
uint gameId
) public view returns (PlayerScoreSheetStruct[] memory Scores) {
uint available;
for (uint i = 1; i <= totalPlayers.current(); i++) {
if (players[i].gameId == gameId) available++;
}
Scores = new PlayerScoreSheetStruct[](available);
uint index;
for (uint i = 1; i <= totalPlayers.current(); i++) {
if (players[i].gameId == gameId) {
Scores[index++] = scores[gameId][players[i].player];
}
}
}
function payout(uint gameId) public {
require(gameExists[gameId], "Game does not exist");
require(currentTime() > games[gameId].endDate, "Game still in session"); // disable rule while testing
require(!games[gameId].paidOut, "Game already paid out");
uint fee = (games[gameId].stake.mul(serviceFee)).div(100);
uint profit = games[gameId].stake.sub(fee);
payTo(owner(), fee);
profit = profit.sub(fee.div(2));
payTo(games[gameId].owner, fee.div(2));
uint available;
for (uint i = 1; i <= totalPlayers.current(); i++) {
if (players[i].gameId == gameId) available++;
}
PlayerScoreSheetStruct[] memory Scores = new PlayerScoreSheetStruct[](
available
);
uint index;
for (uint i = 1; i <= totalPlayers.current(); i++) {
if (players[i].gameId == gameId) {
Scores[index++] = scores[gameId][players[i].player];
}
}
Scores = sortScores(Scores);
for (uint i = 0; i < games[gameId].numberOfWinners; i++) {
uint payoutAmount = profit.div(games[gameId].numberOfWinners);
payTo(Scores[i].player, payoutAmount);
}
games[gameId].paidOut = true;
}
function isPlayerListed(
uint gameId,
address player
) public view returns (bool) {
return isListed[gameId][player];
}
function getMyGames() public view returns (GameStruct[] memory userGames) {
uint available;
for (uint256 i = 1; i <= totalGame.current(); i++) {
if (
games[i].owner == msg.sender &&
!games[i].deleted &&
!games[i].paidOut &&
currentTime() < games[i].endDate
) {
available++;
}
}
userGames = new GameStruct[](available);
uint index;
for (uint256 i = 1; i <= totalGame.current(); i++) {
if (
games[i].owner == msg.sender &&
!games[i].deleted &&
!games[i].paidOut &&
currentTime() < games[i].endDate
) {
userGames[index++] = games[i];
}
}
}
function sortScores(
PlayerScoreSheetStruct[] memory playersScores
) public pure returns (PlayerScoreSheetStruct[] memory) {
uint n = playersScores.length;
for (uint i = 0; i < n - 1; i++) {
for (uint j = 0; j < n - i - 1; j++) {
// Check if the players played before comparing their scores
if (playersScores[j].played && playersScores[j + 1].played) {
if (playersScores[j].score > playersScores[j + 1].score) {
// Swap the elements
PlayerScoreSheetStruct memory temp = playersScores[j];
playersScores[j] = playersScores[j + 1];
playersScores[j + 1] = temp;
}
} else if (
!playersScores[j].played && playersScores[j + 1].played
) {
// Sort players who didn't play below players who played
// Swap the elements
PlayerScoreSheetStruct memory temp = playersScores[j];
playersScores[j] = playersScores[j + 1];
playersScores[j + 1] = temp;
}
}
}
return playersScores;
}
function _saveGame(
uint gameId,
string memory title,
string memory description,
uint participants,
uint numberOfWinners,
uint challenges,
uint startDate,
uint endDate
) internal returns (bool) {
GameStruct memory gameData;
gameData.id = gameId;
gameData.title = title;
gameData.description = description;
gameData.owner = msg.sender;
gameData.participants = participants;
gameData.challenges = challenges;
gameData.stake = msg.value;
gameData.numberOfWinners = numberOfWinners;
gameData.startDate = startDate;
gameData.endDate = endDate;
gameData.timestamp = currentTime();
games[gameId] = gameData;
gameExists[gameId] = true;
return true;
}
function _savePlayer(uint gameId) internal returns (bool) {
totalPlayers.increment();
uint playerId = totalPlayers.current();
players[playerId] = PlayerStruct({
id: playerId,
gameId: gameId,
player: msg.sender
});
isListed[gameId][msg.sender] = true;
playerExists[playerId] = true;
games[gameId].acceptees++;
totalBalance += msg.value;
return true;
}
function currentTime() internal view returns (uint256) {
return (block.timestamp * 1000) + 1000;
}
function payTo(address to, uint256 amount) internal {
(bool success, ) = payable(to).call{value: amount}("");
require(success);
}
}
view raw PlayToEarn.sol hosted with ❤ by GitHub

The PlayToEarn smart contract has been meticulously crafted to streamline game creation, enhance gameplay experiences, manage invitations, and more. Now, let's delve into a comprehensive overview of its fundamental functionalities:

  1. Contract Inheritance and Dependencies:

    • PlayToEarn leverages contracts inherited from the OpenZeppelin library, including well-established modules such as Ownable, Counters, ReentrancyGuard, and SafeMath. These foundational components contribute to the robustness and security of the smart contract system.
  2. Structs:

    • GameStruct : This structure encapsulates game information such as the title, description, owner, participant counts, challenge quantities, gameplay instances, stake amounts, and key dates like startDate and endDate
- PlayerStruct: This holds important info about a player, like their ID, address, and associated GameStruct.

- InvitationStruct: This keeps track of game play invitations, including the invitee's address and other game details.

- PlayerScoreSheetStruct: This stores a player's game data, such as their score and address, along with other game-related info.
Enter fullscreen mode Exit fullscreen mode
  1. State Variables:
    • ServiceFee: This is the fee deducted from every successfully completed game for the platform owner.
- TotalBalance: This keeps track of the total balance accumulated from game stakes.
Enter fullscreen mode Exit fullscreen mode
  1. Mappings:

    • games: Maps game IDs to their respective **GameStruct** data, storing comprehensive game information.
    • players: Connects player IDs to their corresponding **PlayerStruct** details, capturing player-specific data.
    • invitationsOf: Maps player addresses and game IDs to related **InvitationStruct** data, tracking invitations.
    • isListed: Maps game IDs and player addresses to indicate player listing for specific games.
    • gameExists: Tracks the existence of games based on their IDs.
    • playerExists: Records the presence of players using their unique IDs.
    • invitationExists: Tracks the existence of invitations for specific games and players.
    • scores: Associates game IDs and player addresses with their respective **PlayerScoreSheetStruct** data, storing gameplay records.
    • gameHasPlayers: Indicates whether a game has associated players.
  2. Game Functions*:*

    • createGame: Initiates a new game by creating a GameStruct with essential details.
    • getGames: Retrieves an array of active game structures to provide an overview of ongoing games.
    • getGame: Retrieves details of a specific game using its ID.
    • payout: Manages the payout process for a completed game, distributing rewards to winners.
  3. Invitation Functions*:*

    • invitePlayer: Facilitates the invitation of players to join a game, recording invitation details.
    • acceptInvitation: Enables players to accept game invitations, validating their eligibility and recording their participation.
    • rejectInvitation: Allows players to decline game invitations, marking the invitation as rejected.
    • getInvitations: Retrieves an array of invitations sent to a specific player.
  4. Score Functions*:*

    • recordScore: Enables players to record their scores for a specific game, provided certain conditions are met.
    • getScores: Retrieves an array of **PlayerScoreSheetStruct** for a particular game, showcasing player scores.
  5. Player Functions*:*

    • isPlayerListed: Checks whether a player is listed for a specific game.
    • getMyGames: Retrieves an array of games owned by the caller.
  6. Internal Utility Functions*:*

    • _saveGame: Internal function for saving game data into the **games** mapping during game creation.
    • _savePlayer: Internal function for saving player data into the **players** mapping during various interactions.
    • currentTime: Returns the current timestamp with adjusted precision.
    • payTo: Facilitates transferring funds to a specific address.

    These functions cover a wide range of functionalities, efficiently managing game creation, invitations, gameplay, scores, and player interactions within the PlayToEarn contract.

🚀 Elevate your web3 skills with Dapp Mentors Academy – the ultimate hub for blockchain developers! Access premium courses, NFT insights, and a special Discord channel for just $8.44/month. Join us now to unleash your potential!

Dapp Mentors Academy

Subscribe to Dapp Mentors Academy today and get exclusive access to over 40 hours of web3 content, including courses on NFT minting, blockchain development, and more!

The Test Script

The PlayToEarn test script has been thoughtfully crafted to thoroughly evaluate and confirm the functionalities and behaviors of the PlayToEarn smart contract. Here's an organized breakdown of the primary tests and functions encompassed within the script:

  1. Test Setup:

    • The script doesn’t require any parameter for the contract deployment.
    • It sets up deployer and three (3) user addresses for testing purposes.
  2. Contract Deployment and Minting:

    • The script deploys the PlayToEarn contract using the specified parameters.
    • It uses the createGame function to create a game with a stake value of 0.5 Ether.
    • The test checks the successful creation of game and verifies the retrieved game.
  3. Game Creation:

    • This section encapsulates a series of tests under the **describe** block labeled 'Game creation', focusing on evaluating various aspects of game creation and management within the PlayToEarn smart contract.
-  The first test, labeled 'should confirm fetching games', utilizes the `**getGames**` function to retrieve a list of games. The Chai assertion `**expect(result).to.have.lengthOf(1)**` validates that one game has been successfully created.

- The subsequent test, 'should confirm fetching a single game', employs the `**getGame**` function to retrieve the attributes of a specific game using its `**gameId**`. The assertion `**expect(result.id).to.be.equal(1)**` verifies that the retrieved game's ID matches the expected value.

- The third test, 'should confirm listing of a players invitations', showcases the invitation process. It verifies that initially, there are no invitations, then invites `**user1**` and `**user2**` to the game, checks the number of invitations for `**user1**`, accepts the invitation for `**user1**`, and confirms the listing status of `**user1**`. Finally, it rejects the invitation for `**user2**` and confirms that `**user2**` is not listed.

- The last test in this section, 'should confirm payouts', assesses the payout mechanism. The test invites `**user1**`, accepts the invitation, records scores, and verifies that the game has not been paid out. Upon executing the payout function, the test checks that the game has been successfully paid out.
Enter fullscreen mode Exit fullscreen mode

Through this detailed breakdown, the essential functionalities of the PlayToEarn test script are explained, emphasizing the purpose and outcomes of each testing step.

At the root of the project, create a folder if not existing called “test”, copy and paste the code below inside of it.

const { expect } = require('chai')
const toWei = (num) => ethers.utils.parseEther(num.toString())
describe('Contracts', () => {
let contract, result
const description = 'showcase your speed in a game'
const title = 'Game title'
const participants = 4
const winners = 1
const challenges = 5
const starts = Date.now() - 10 * 60 * 1000
const ends = Date.now() + 10 * 60 * 1000
const stake = 0.5
const gameId = 1
beforeEach(async () => {
const Contract = await ethers.getContractFactory('PlayToEarn')
;[deployer, user1, user2, user3] = await ethers.getSigners()
contract = await Contract.deploy()
await contract.deployed()
})
beforeEach(async () => {
await contract.createGame(
title,
description,
participants,
winners,
challenges,
starts,
ends,
{
value: toWei(stake),
}
)
})
describe('Game creation', () => {
it('should confirm fetching games', async () => {
result = await contract.getGames()
expect(result).to.have.lengthOf(1)
})
it('should confirm fetching a single game', async () => {
result = await contract.getGame(gameId)
expect(result.id).to.be.equal(1)
})
it('should confirm listing of a players invitations', async () => {
result = await contract.connect(user1).getInvitations()
expect(result).to.have.lengthOf(0)
await contract.invitePlayer(user1.address, gameId)
await contract.invitePlayer(user2.address, gameId)
result = await contract.connect(user1).getInvitations()
expect(result).to.have.lengthOf(1)
await contract.connect(user1).acceptInvitation(1, {
value: toWei(stake),
})
result = await contract.isPlayerListed(gameId, user1.address)
expect(result).to.be.true
await contract.connect(user2).rejectInvitation(gameId)
result = await contract.isPlayerListed(gameId, user2.address)
expect(result).to.be.false
})
it('should confirm payouts', async () => {
await contract.invitePlayer(user1.address, gameId)
await contract.connect(user1).acceptInvitation(gameId, {
value: toWei(stake),
})
await contract.recordScore(gameId, 23)
await contract.connect(user1).recordScore(gameId, 19)
result = await contract.getGame(gameId)
expect(result.paidOut).to.be.false
await contract.payout(gameId)
result = await contract.getGame(gameId)
expect(result.paidOut).to.be.true
})
})
})

By running **yarn hardhat test** on the terminal will test out all the essential function of this smart contract.

The Deployment Script

The PlayToEarn deployment script is responsible for deploying the PlayToEarn smart contract to the Ethereum network using the Hardhat development environment. Here's an overview of the script:

  1. Import Statements:

    • The script imports the required dependencies, including ethers and the fs module for file system operations.
  2. main() Function:

    • The main() function is an asynchronous function that serves as the entry point for the deployment script.
  3. Deployment Parameters:

    • The contract requires only the contract_name parameter for deployment
  4. Contract Deployment:

    • The script uses the ethers.getContractFactory() method to obtain the contract factory for the PlayToEarn contract.
    • It deploys the contract by invoking the deploy() method on the contract factory with the specified parameters.
    • The deployed contract instance is stored in the contract variable.
  5. Contract Deployment Confirmation:

    • The script waits for the deployment to be confirmed by awaiting the deployed() function on the contract instance.
  6. Writing Contract Address to File:

    • The script creates a JSON object containing the deployed contract address.
    • It writes this JSON object to a file named contractAddress.json in the specified path: ./src/abis/contractAddress.json.
    • If any error occurs during the file writing process, it is logged to the console.
  7. Logging Deployed Contract Address:

    • If the contract deployment and file writing processes are successful, the deployed contract address is logged to the console.
  8. Error Handling:

    • Any errors that occur during the deployment or file writing process are caught and logged to the console.
    • The process exit code is set to 1 to indicate an error occurred.

The PlayToEarn deployment script allows for the easy deployment of the PlayToEarn smart contract, and it generates a JSON file containing the deployed contract address for further usage within the project.

In the root of the project, create a folder called “scripts” and another file inside of it called deploy.js if it doesn’t yet exist. Copy and paste the code below inside of it.

const { ethers } = require('hardhat')
const fs = require('fs')
const toWei = (num) => ethers.utils.parseEther(num.toString())
async function main() {
const contract_name = 'PlayToEarn'
const Contract = await ethers.getContractFactory(contract_name)
const contract = await Contract.deploy()
await contract.deployed()
const address = JSON.stringify({ address: contract.address }, null, 4)
fs.writeFile('./src/abis/contractAddress.json', address, 'utf8', (err) => {
if (err) {
console.error(err)
return
}
console.log('Deployed contract address', contract.address)
})
}
main().catch((error) => {
console.error(error)
process.exitCode = 1
})
view raw deploy.js hosted with ❤ by GitHub

Next, run the yarn hardhat run scripts/deploy.js to deploy the smart contract into the network on a terminal.

Activities of Deployment on the Terminal

If you require additional assistance with setting up Hardhat or deploying your Fullstack DApp, I recommend watching this informative video that provides guidance and instructions.

Developing the Frontend

To start developing the frontend of our application, we will create a new folder called components inside the src directory. This folder will hold all the components needed for our project.

For each of the components listed below, you will need to create a corresponding file inside the src/components folder and paste its codes inside it.

Header Component

The Header Component

The header component of the PlayToEarn application’s user interface displays the app's logo, important links for easy movement around the app, and a special button that lets users connect their wallet. This button takes care of the technical stuff needed to link the user's wallet address. You can take a look at the code example below to see how it works:

import React from 'react'
import { Link } from 'react-router-dom'
import { truncate, useGlobalState } from '../store'
import { connectWallet } from '../services/blockchain'
const Header = () => {
const [connectedAccount] = useGlobalState('connectedAccount')
return (
<header className="bg-white shadow-sm shadow-gray-300 p-2">
<main className="w-11/12 mx-auto p-2 flex justify-between items-center flex-wrap">
<Link to={'/'} className="text-2xl mb-2">
Play2<span className="text-blue-700">Earn</span>
</Link>
<div className="flex justify-end items-center space-x-2 md:space-x-4 mt-2 md:mt-0">
<Link to={'/mygames'} className="text-md">
My Games
</Link>
<Link to={'/invitations'} className="text-md">
Invitations
</Link>
{connectedAccount ? (
<button
className="bg-blue-700 text-white py-2 px-3 md:py-2 md:px-5 rounded-full
hover:bg-blue-600 duration-200 transition-all shadow-md shadow-black"
>
{truncate(connectedAccount, 4, 4, 11)}
</button>
) : (
<button
onClick={connectWallet}
className="bg-blue-700 text-white py-2 px-3 md:py-2 md:px-5 rounded-full
hover:bg-blue-600 duration-200 transition-all shadow-md shadow-black"
>
Connect Wallet
</button>
)}
</div>
</main>
</header>
);
}
export default Header
view raw Header.jsx hosted with ❤ by GitHub

Hero Component

The Hero Component

The Hero component within the PlayToEarn application takes the stage with an engaging tagline, setting the tone for a welcoming introduction. Additionally, it hosts call-to-action buttons designed to kickstart the game creation process and view of user’s involvement in a game. See the code below:

import React from 'react'
import { setGlobalState } from '../store'
import { Link } from 'react-router-dom'
const Hero = () => {
return (
<section className="h-[89vh]">
<main className="flex flex-col justify-center items-center h-full">
<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
onClick={() => setGlobalState('createModal', 'scale-100')}
className="bg-blue-700 border-[1px] text-white py-3 px-5 duration-200 transition-all hover:bg-blue-600"
>
Create Game
</button>
<Link
to="/mygames"
className="border-[1px] border-blue-700 text-blue-700 py-3 px-5 duration-200 transition-all hover:bg-blue-700 hover:text-white"
>
My Games
</Link>
</div>
</main>
</section>
)
}
export default Hero
view raw Hero.jsx hosted with ❤ by GitHub

Create Game Component

The Create Game Component

The create game component is a modal component that allows users to add a game to the platform after providing relevant and useful information required by the system for game creation. See code below

import React, { useState } from 'react'
import { FaTimes } from 'react-icons/fa'
import { setGlobalState, useGlobalState } from '../store'
import { createGame } from '../services/blockchain'
import { toast } from 'react-toastify'
import { useNavigate } from 'react-router-dom'
const CreateGame = () => {
const [createModal] = useGlobalState('createModal')
const navigate = useNavigate()
const [game, setGame] = useState({
title: '',
description: '',
participants: '',
winners: '',
challenges: '',
starts: '',
ends: '',
stake: '',
})
const handleChange = (e) => {
const { name, value } = e.target
setGame((prevState) => ({
...prevState,
[name]: value,
}))
}
const closeModal = () => {
setGlobalState('createModal', 'scale-0')
setGame({
title: '',
participants: '',
winners: '',
challenges: '',
starts: '',
ends: '',
description: '',
stake: '',
})
}
const handleGameCreation = async (e) => {
e.preventDefault()
game.starts = new Date(game.starts).getTime()
game.ends = new Date(game.ends).getTime()
await toast.promise(new Promise(async (resolve, reject) => {
await createGame(game)
.then((tx)=>{
console.log(tx)
closeModal()
resolve(tx)
navigate('/mygames')
})
.catch((err)=>{
reject(err)
})
}),
{
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-white text-black shadow-lg shadow-blue-500 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}
>
<label className="text-[12px]">Title</label>
<div className="py-2 w-full border border-[#212D4A] 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]">Participants</label>
<div className="py-2 w-full border border-[#212D4A] 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-[#212D4A] 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="winners"
value={game.winners}
onChange={handleChange}
required
/>
</div>
</div>
<div className="w-full">
<label className="text-[12px]">Number of challenges</label>
<div className="py-2 w-full border border-[#212D4A] rounded-full flex items-center px-4">
<input
placeholder="E.g 5"
type="number"
min={5}
className="bg-transparent outline-none w-full placeholder-[#3D3857] text-sm border-none focus:outline-none focus:ring-0 py-0"
name="challenges"
value={game.challenges}
onChange={handleChange}
required
/>
</div>
</div>
</div>
<div className="w-full">
<label className="text-[12px]">Stake amount</label>
<div className="py-2 w-full border border-[#212D4A] rounded-full flex items-center px-4 mb-3 mt-2">
<input
placeholder="eg 0.04"
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}
type="number"
onChange={handleChange}
step={0.0001}
min={0.0001}
required
/>
</div>
</div>
<label className="text-[12px]">Starts On</label>
<div className="py-2 w-full border border-[#212D4A] 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="starts"
type="datetime-local"
value={game.starts}
onChange={handleChange}
required
/>
</div>
<label className="text-[12px]">Ends On</label>
<div className="py-2 w-full border border-[#212D4A] 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="ends"
type="datetime-local"
value={game.ends}
onChange={handleChange}
required
/>
</div>
<label className="text-[12px]">Description</label>
<textarea
placeholder="What is this game about?"
className="h-[70px] w-full bg-transparent border border-[#212D4A] 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="text-sm bg-blue-700 rounded-full w-[150px] h-[48px] text-white
mt-5 hover:bg-blue-500 transition-colors duration-300"
>
Save
</button>
</form>
</div>
</div>
</div>
);
}
export default CreateGame
view raw CreateGame.jsx hosted with ❤ by GitHub

GameList Component

Game list Component

This component handles how games are shown on the platform, like cards organized in a neat grid on the screen. Take a look at the code below to see how it works:

import React from 'react'
import { formatDate, setGlobalState, truncate } from '../store'
import { Link } from 'react-router-dom'
const GameList = ({ games }) => {
const handleInviteClick = (game) => {
setGlobalState('game', game)
setGlobalState('inviteModal', 'scale-100')
}
return (
<div className="w-3/5 mx-auto my-10">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{games.length > 0 ? (
games.map((game) => (
<div key={game.id} className="bg-white p-6 rounded-lg shadow-md">
<h3 className="text-lg font-semibold mb-2">{game.title}</h3>
<p className="text-gray-600 mb-2">{game.description}</p>
<p className="text-gray-600">
Owner: {truncate(game.owner, 4, 4, 11)}
</p>
<p className="text-gray-600">
Starts on: {formatDate(game.startDate)}
</p>
<div className="flex justify-start items-center space-x-2 mt-3">
<Link
to={'/gameplay/' + game.id}
className="bg-red-700 text-white py-2 px-5 rounded-full
hover:bg-red-600 duration-200 transition-all"
>
View
</Link>
<button
onClick={() => handleInviteClick(game)}
className="bg-blue-700 text-white py-2 px-5 rounded-full
hover:bg-blue-600 duration-200 transition-all"
>
Invite
</button>
</div>
</div>
))
) : (
<div className="text-lg font-semibold">No games yet</div>
)}
</div>
</div>
)
}
export default GameList
view raw GameList.jsx hosted with ❤ by GitHub

InviteModal Component

The Invite Modal

This component plays an essential role in inviting players for an available game on the platform for participation, It provides an input field where the user inviting has to input the invitees wallet address for sending a join game request . See code below:

import React, { useState } from 'react'
import { FaTimes } from 'react-icons/fa'
import { setGlobalState, useGlobalState } from '../store'
import { toast } from 'react-toastify'
import { invitePlayer } from '../services/blockchain'
const InviteModal = () => {
const [player, setPlayer] = useState('')
const [game] = useGlobalState('game')
const [inviteModal] = useGlobalState('inviteModal')
const sendInvitation = async (e) => {
e.preventDefault()
await toast.promise(
new Promise(async (resolve, reject) => {
await invitePlayer(player, game.id)
.then((tx) => {
console.log(tx)
closeModal()
resolve(tx)
})
.catch((err) => {
reject(err)
})
}),
{
pending: 'Approve transaction...',
success: 'Invitation sent successful 👌',
error: 'Encountered error 🤯',
}
)
}
const closeModal = () => {
setGlobalState('inviteModal', '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-white text-black shadow-lg shadow-blue-500 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="text-sm bg-blue-700 rounded-full w-[150px] h-[48px] text-white
hover:bg-blue-500 transition-colors duration-300"
>
Send Invite
</button>
</form>
</div>
</div>
</div>
)
}
export default InviteModal
view raw InviteModal.jsx hosted with ❤ by GitHub

InvitationList Component

The Invitation List Component

This component takes charge of displaying invitations of a particular user to them in a very cool and neat way, as shown above, with this the user can accept or reject an invitation with the click of a button. See code below:

import React from 'react'
import { toast } from 'react-toastify'
import { acceptInvitation, rejectInvitation } from '../services/blockchain'
import { Link } from 'react-router-dom'
const InvitationList = ({ invitations }) => {
const handleAcceptance = async (invitation) => {
await toast.promise(
new Promise(async (resolve, reject) => {
await acceptInvitation(invitation.gameId, invitation.stake)
.then((tx) => {
console.log(tx)
resolve(tx)
})
.catch((err) => {
reject(err)
})
}),
{
pending: 'Approve transaction...',
success: 'Invitation accepted successful 👌',
error: 'Encountered error 🤯',
}
)
}
const handleRejection = async (invitation) => {
await toast.promise(
new Promise(async (resolve, reject) => {
await rejectInvitation(invitation.gameId)
.then((tx) => {
console.log(tx)
resolve(tx)
})
.catch((err) => {
reject(err)
})
}),
{
pending: 'Approve transaction...',
success: 'Invitation rejected successful 👌',
error: 'Encountered error 🤯',
}
)
}
return (
<div className="w-3/5 mx-auto my-10">
<h2 className="text-2xl font-semibold text-gray-800 mb-4">
Invitations List
</h2>
{invitations.map((invitation, i) => (
<div
key={i}
className="bg-white rounded-md shadow-md p-4 mb-4 flex justify-between items-center"
>
<div>
<p
className={`font-semibold ${
invitation.responded && !invitation.accepted
? 'line-through italic text-gray-600'
: 'text-gray-800'
}`}
>
{invitation.accepted ? (
<span>
"{invitation.title}" game is yours to play
</span>
) : (
<span>
You've been invited to the "{invitation.title}" game
</span>
)}
</p>
</div>
{!invitation.responded && (
<div className="space-x-4">
<button
onClick={() => handleAcceptance(invitation)}
className="bg-blue-700 text-white py-2 px-5 rounded-full
hover:bg-blue-600 duration-200 transition-all"
>
Accept
</button>
<button
onClick={() => handleRejection(invitation)}
className="bg-red-700 text-white py-2 px-5 rounded-full
hover:bg-red-600 duration-200 transition-all"
>
Reject
</button>
</div>
)}
{invitation.accepted && (
<Link
to={'/gameplay/' + invitation.gameId}
className="bg-blue-700 text-white py-2 px-5 rounded-full
hover:bg-blue-600 duration-200 transition-all"
>
Play Game
</Link>
)}
</div>
))}
</div>
);
}
export default InvitationList

Game Component

Game Play Component

This component serves as the central hub for two integral sub-components, enhancing the overall gaming experience. The key constituents are as follows:

  • GameInfo Component: This component assumes the responsibility of imparting essential instructions and guidelines for the game. It plays a pivotal role in acquainting players with crucial information necessary to excel in the game.
  • ChatButton Component: leverages CometChat to seamlessly connect players within a game. It orchestrates signups, logins, group creation for game owners, and group joining for players. This component also triggers the chat modal, facilitating social interaction and togetherness among players, enhancing their gaming experience.

In addition to its pivotal role in information dissemination and fostering player connections, this component maintains its significance in fundamental game operations. It seamlessly handles game initiation, active gameplay, results presentation, and payout distribution.

import { EmojtCha } from 'emojtcha-react'
import { useState, useEffect } from 'react'
import ChatButton from './ChatButton'
import GameInfo from './GameInfo'
import { toast } from 'react-toastify'
import { payout, recordScore } from '../services/blockchain'
import { useNavigate } from 'react-router-dom'
import { setGlobalState } from '../store'
export default function Game({ game, isPlayed }) {
const numEmojtChas = game.challenges
const navigate = useNavigate()
const [validationStates, setValidationStates] = useState(
Array(numEmojtChas).fill(false)
)
const [revealedIndex, setRevealedIndex] = useState(0)
const [startTime, setStartTime] = useState(null)
const [endTime, setEndTime] = useState(null)
const [timerStarted, setTimerStarted] = useState(false)
const handleSelect = (index, isSelected) => {
const newValidationStates = [...validationStates]
newValidationStates[index] = isSelected
setValidationStates(newValidationStates)
if (isSelected && revealedIndex < numEmojtChas - 1) {
setRevealedIndex(revealedIndex + 1)
} else if (isSelected && revealedIndex === numEmojtChas - 1) {
setEndTime(new Date())
}
}
const resetGame = () => {
setValidationStates(Array(numEmojtChas).fill(false))
setRevealedIndex(0)
setStartTime(null)
setEndTime(null)
setTimerStarted(false)
}
const allCaptchasPassed = validationStates.every((state) => state)
useEffect(() => {
if (revealedIndex === 0 && timerStarted) {
setStartTime(new Date())
}
}, [revealedIndex, timerStarted])
const calculateElapsedTime = () => {
if (startTime && endTime) {
const elapsedMilliseconds = endTime - startTime
const elapsedSeconds = Math.floor(elapsedMilliseconds / 1000)
return elapsedSeconds
}
return 0
}
const submitScore = async () => {
await toast.promise(
new Promise(async (resolve, reject) => {
await recordScore(game.id, calculateElapsedTime())
.then((tx) => {
console.log(tx)
resolve(tx)
navigate('/mygames')
})
.catch((err) => {
reject(err)
})
}),
{
pending: 'Approve transaction...',
success: 'Score submittion successful 👌',
error: 'Encountered error 🤯',
}
)
}
const handlePayout = async () => {
await toast.promise(
new Promise(async (resolve, reject) => {
await payout(game.id)
.then((tx) => {
console.log(tx)
resolve(tx)
})
.catch((err) => {
reject(err)
})
}),
{
pending: 'Approve transaction...',
success: 'Paid out successful 👌',
error: 'Encountered error 🤯',
}
)
}
return (
<div className="min-h-screen flex flex-col justify-center items-center">
{timerStarted &&
validationStates.map((isValidationPassed, index) => (
<div
key={index}
className={`${
index === revealedIndex ? 'block' : 'hidden'
} p-4 border rounded shadow bg-white`}
>
<h1 className="text-xl font-semibold text-center mb-2">
Emoji {isValidationPassed ? 'passed' : 'not passed'}
</h1>
<div className="flex justify-center items-center h-32">
<EmojtCha
drawCount={6}
onSelect={(isSelected) => handleSelect(index, isSelected)}
/>
</div>
</div>
))}
{!timerStarted && (
<div className="flex flex-col justify-center items-center space-y-4 px-5">
<GameInfo game={game} />
<div className="flex justify-center items-center space-x-2">
{Date.now() > game.startDate &&
Date.now() < game.endDate &&
game.acceptees >= game.numberOfWinners + 1 &&
!isPlayed && (
<button
className="bg-blue-700 text-white py-2 px-4 rounded
hover:bg-blue-600 duration-200 transition-all shadow-md shadow-black"
onClick={() => setTimerStarted(true)}
>
Play
</button>
)}
{Date.now() > game.endDate && (
<>
<button
className="bg-green-700 text-white py-2 px-4 rounded
hover:bg-green-600 duration-200 transition-all shadow-md shadow-black"
onClick={() => setGlobalState('resultModal', 'scale-100')}
>
Result
</button>
{!game.paidOut && (
<button
className="bg-orange-700 text-white py-2 px-4 rounded
hover:bg-orange-600 duration-200 transition-all shadow-md shadow-black"
onClick={handlePayout}
>
Payout
</button>
)}
</>
)}
<ChatButton gid={game?.id} />
</div>
</div>
)}
{allCaptchasPassed && (
<div className="mt-4 p-4 border rounded shadow bg-white">
{/* <p className="text-lg text-center mb-2">
Time taken: {calculateElapsedTime()} seconds
</p> */}
<div className="flex justify-between items-center space-x-2">
<button
className="bg-green-500 text-white py-2 px-4 rounded
hover:bg-green-700 mt-2 w-full shadow-md shadow-black"
onClick={submitScore}
>
Submit
</button>
<button
className="bg-red-500 text-white py-2 px-4 rounded
hover:bg-red-700 mt-2 w-full shadow-md shadow-black"
onClick={resetGame}
>
Restart
</button>
</div>
</div>
)}
</div>
)
}
view raw Game.jsx hosted with ❤ by GitHub
import React from 'react'
import { timestampToDate, truncate } from '../store'
const GameInfo = ({ game }) => {
return (
<div className="bg-white p-6 rounded-lg shadow-md sm:w-2/5">
<h3 className="text-lg font-semibold mb-2">
{game.title} (Instructions)
</h3>
<p className="text-gray-600">
This game is hosted by{' '}
<span className="font-medium text-black">
{truncate(game.owner, 4, 4, 11)}
</span>
, with{' '}
<span className="font-medium text-black">
{game.participants} participants
</span>{' '}
joining from the globe and{' '}
<span className="font-medium text-black">
{game.acceptees} person(s){' '}
</span>
already onboarded.
<br />
<br />
We have{' '}
<span className="font-medium text-black">
{game.challenges} challenges
</span>{' '}
to be complete in this game, and the rewards of{' '}
<span className="font-medium text-black">
{game.stake * game.participants} ETH
</span>{' '}
will be shared amongst the{' '}
<span className="font-medium text-black">
{game.numberOfWinners} person(s)
</span>{' '}
to emerge as winners.
<br />
<br />
This game is scheduled for{' '}
<span className="font-medium text-black">
{timestampToDate(game.startDate)} - {timestampToDate(game.endDate)}
</span>{' '}
and so far,
<span className="font-medium text-black">
{' '}
{game.plays} person(s)
</span>{' '}
have played and{' '}
<span className="font-medium text-black">
is {!game.paidOut && ' yet to be '} paidout
</span>
.
</p>
</div>
)
}
export default GameInfo
view raw GameInfo.jsx hosted with ❤ by GitHub
import React, { useEffect } from 'react'
import { MdOutlineChat } from 'react-icons/md'
import { FiLogIn } from 'react-icons/fi'
import { HiLogin } from 'react-icons/hi'
import { FiUsers } from 'react-icons/fi'
import { AiFillLock } from 'react-icons/ai'
import { Menu } from '@headlessui/react'
import { toast } from 'react-toastify'
import {
createNewGroup,
joinGroup,
logOutWithCometChat,
loginWithCometChat,
signUpWithCometChat,
} from '../services/chat'
import { setGlobalState, useGlobalState } from '../store'
import { IoMdPeople, IoIosAddCircle } from 'react-icons/io'
const ChatButton = ({ gid }) => {
const [connectedAccount] = useGlobalState('connectedAccount')
const [currentUser] = useGlobalState('currentUser')
const [game] = useGlobalState('game')
const [group] = useGlobalState('group')
const handleSignUp = async () => {
await toast.promise(
new Promise(async (resolve, reject) => {
await signUpWithCometChat(connectedAccount)
.then((user) => resolve(user))
.catch((error) => {
alert(JSON.stringify(error))
reject(error)
})
}),
{
pending: 'Signning up...',
success: 'Signed up successfully, please login 👌',
error: 'Encountered error 🤯',
}
)
}
const handleLogin = async () => {
await toast.promise(
new Promise(async (resolve, reject) => {
await loginWithCometChat(connectedAccount)
.then((user) => {
setGlobalState('currentUser', user)
resolve(user)
})
.catch((error) => {
alert(JSON.stringify(error))
reject(error)
})
}),
{
pending: 'Logging...',
success: 'Logged in successfully 👌',
error: 'Encountered error 🤯',
}
)
}
const handleLogout = async () => {
await toast.promise(
new Promise(async (resolve, reject) => {
await logOutWithCometChat()
.then(() => {
setGlobalState('currentUser', null)
resolve()
})
.catch((error) => {
alert(JSON.stringify(error))
reject(error)
})
}),
{
pending: 'Leaving...',
success: 'Logged out successfully 👌',
error: 'Encountered error 🤯',
}
)
}
const handleCreateGroup = async () => {
await toast.promise(
new Promise(async (resolve, reject) => {
await createNewGroup(`guid_${gid}`, 'game.title')
.then((group) => {
setGlobalState('group', group)
resolve(group)
window.location.reload()
})
.catch((error) => {
alert(JSON.stringify(error))
reject(error)
})
}),
{
pending: 'Creating group...',
success: 'Group created successfully 👌',
error: 'Encountered error 🤯',
}
)
}
const handleJoinGroup = async () => {
await toast.promise(
new Promise(async (resolve, reject) => {
await joinGroup(`guid_${gid}`)
.then((group) => {
setGlobalState('group', group)
resolve()
window.location.reload()
})
.catch((error) => {
alert(JSON.stringify(error))
reject(error)
})
}),
{
pending: 'Joining group...',
success: 'Group joined successfully 👌',
error: 'Encountered error 🤯',
}
)
}
return (
<Menu className="relative" as="div">
<Menu.Button
className="bg-white text-blue-700 py-2 px-4 rounded flex justify-start items-center space-x-1
hover:bg-blue-600 hover:text-white transition-all duration-300 shadow-md shadow-black"
as="button"
>
<MdOutlineChat size={20} /> <span>Chat</span>
</Menu.Button>
<Menu.Items
className="absolute right-0 bottom-14 mt-2 w-56 origin-top-right
divide-y divide-gray-100 rounded-md bg-white shadow-lg shadow-black
ing-1 ring-black ring-opacity-5 focus:outline-none"
>
{!currentUser ? (
<>
<Menu.Item>
{({ active }) => (
<button
className={`flex justify-start items-center space-x-1 ${
active ? 'bg-gray-200 text-black' : 'text-blue-500'
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
onClick={handleSignUp}
>
<AiFillLock size={17} />
<span>Sign Up</span>
</button>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<button
className={`flex justify-start items-center space-x-1 ${
active ? 'bg-gray-200 text-black' : 'text-gray-900'
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
onClick={handleLogin}
>
<FiLogIn size={17} />
<span>Login</span>
</button>
)}
</Menu.Item>
</>
) : !group && currentUser.uid.toLowerCase() == game.owner ? (
<>
<Menu.Item>
{({ active }) => (
<button
className={`flex justify-start items-center space-x-1 ${
active ? 'bg-gray-200 text-black' : 'text-gray-900'
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
onClick={handleCreateGroup}
>
<IoIosAddCircle size={17} />
<span>Create Group</span>
</button>
)}
</Menu.Item>
</>
) : !group?.hasJoined && currentUser ? (
<>
<Menu.Item>
{({ active }) => (
<button
className={`flex justify-start items-center space-x-1 ${
active ? 'bg-gray-200 text-black' : 'text-gray-900'
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
onClick={handleJoinGroup}
>
<IoMdPeople size={17} />
<span>Join Group</span>
</button>
)}
</Menu.Item>
</>
) : (
<>
<Menu.Item>
{({ active }) => (
<button
className={`flex justify-start items-center space-x-1 ${
active ? 'bg-gray-200 text-black' : 'text-gray-900'
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
onClick={() => setGlobalState('chatModal', 'scale-100')}
>
<FiUsers size={17} />
<span>Live Chats</span>
</button>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<button
className={`flex justify-start items-center space-x-1 ${
active ? 'bg-gray-200 text-black' : 'text-gray-900'
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
onClick={handleLogout}
>
<HiLogin size={17} />
<span>Logout</span>
</button>
)}
</Menu.Item>
</>
)}
</Menu.Items>
</Menu>
)
}
export default ChatButton
view raw ChatButton.jsx hosted with ❤ by GitHub

GameResult Component

Game Result Modal

This component serves as a dynamic modal for displaying game results. The component leverages several external libraries and custom functions to create an immersive and visually appealing presentation of player scores and their performance in the game.

import React from 'react'
import { FaTimes } from 'react-icons/fa'
import Identicon from 'react-identicons'
import { setGlobalState, truncate, useGlobalState } from '../store'
const GameResult = ({ game, scores }) => {
const [resultModal] = useGlobalState('resultModal')
const closeModal = () => {
setGlobalState('resultModal', '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}`}
>
<div className="bg-white text-black shadow-lg shadow-blue-500 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">Player Scores</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">
{scores.map((score, i) => (
<div
key={i}
className="bg-white flex justify-between border-b p-4 w-full"
>
<div className="flex justify-start items-center space-x-2">
<Identicon
string={score.player}
size={25}
className="rounded-full shadow-md"
/>
<strong>{truncate(score.player, 4, 4, 11)}</strong>
</div>
{i + 1 <= game.numberOfWinners ? (
<span className="text-green-600">
Finished at {score.score} sec.
</span>
) : (
<span className="text-red-600">
{score.played
? `Finished at ${score.score} sec.`
: `Absent`}
</span>
)}
</div>
))}
</div>
</div>
</div>
</div>
)
}
export default GameResult
view raw GameResult.jsx hosted with ❤ by GitHub

Chat Component

The Chat Component

This React component serves as an interactive chat interface, facilitating seamless real-time communication between users. Employing a blend of essential dependencies and specialized services, this component crafts a dynamic and user-centric chat environment.

import React, { useEffect, useState } from 'react'
import Identicon from 'react-identicons'
import { FaTimes } from 'react-icons/fa'
import { setGlobalState, truncate, useGlobalState } from '../store'
import { useParams } from 'react-router-dom'
import { getMessages, listenForMessage, sendMessage } from '../services/chat'
const Chat = () => {
const [connectedAccount] = useGlobalState("connectedAccount");
const [chatModal] = useGlobalState("chatModal");
const [messages] = useGlobalState("messages");
const [message, setMessage] = useState("");
const { id } = useParams();
const onSendMessage = async (e) => {
e.preventDefault();
if (!message) return;
await sendMessage(`guid_${id}`, message).then((msg) => {
setGlobalState("messages", (prevState) => [...prevState, msg]);
console.log(msg);
setMessage("");
scrollToEnd();
});
};
useEffect(() => {
const fetchData = async () => {
await getMessages(`guid_${id}`).then((msgs) => {
setGlobalState('messages', msgs)
scrollToEnd()
})
await listenForMessage(`guid_${id}`).then((msg) => {
setGlobalState('messages', (prevState) => [...prevState, msg])
scrollToEnd()
})
}
fetchData()
}, [])
const scrollToEnd = () => {
const elmnt = document.getElementById("messages-container");
elmnt.scrollTop = elmnt.scrollHeight;
};
const closeModal = () => {
setGlobalState("chatModal", "scale-0");
setMessage("");
};
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 ${chatModal}`}
>
<div className="bg-white text-black shadow-lg shadow-blue-500 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">Chat</p>
<button
onClick={closeModal}
className="border-0 bg-transparent focus:outline-none"
>
<FaTimes />
</button>
</div>
<div className="flex flex-col">
<div
id="messages-container"
className="flex flex-col justify-center items-start rounded-xl my-5 pt-5 max-h-[20rem] overflow-y-auto"
>
{messages.length < 1 && <p>No Chat yet...</p>}
{messages.map((msg, i) => (
<Message
text={msg.text}
owner={msg.sender.uid}
time={Number(msg.sentAt + "000")}
you={connectedAccount == msg.sender.uid}
key={i}
/>
))}
</div>
<form onSubmit={onSendMessage} className="h-[4rem] w-full mt-4">
<input
value={message}
onChange={(e) => setMessage(e.target.value)}
className="h-full w-full p-5 focus:outline-none focus:ring-0 rounded-md
placeholder-gray-400 bg-transparent border border-gray-400"
placeholder="Leave a message..."
/>
</form>
</div>
</div>
</div>
</div>
);
}
const Message = ({ text, time, owner, you }) => {
return (
<div className="flex justify-between items-end space-x-4 px-6 mb-4 w-full">
<div className="flex justify-start items-center">
<Identicon
className="w-12 h-12 rounded-full object-cover mr-4 shadow-md bg-gray-400"
string={owner}
size={30}
/>
<div>
<h3 className="text-md font-bold">
{you ? '@You' : truncate(owner, 4, 4, 11)}
</h3>
<p className="flex flex-col text-gray-500 text-xs font-semibold">
{text}
</p>
</div>
</div>
<span className="text-xs">{new Date(time).toLocaleString()}</span>
</div>
)
}
export default Chat
view raw Chat.jsx hosted with ❤ by GitHub

Want to learn how to build an Answer-To-Earn DApp with Next.js, TypeScript, Tailwind CSS, and Solidity? Watch this video now!

This video is a great resource for anyone who wants to learn how to build decentralized applications and earn ethers.

Now that we have covered all the components in this application, it is time to start coupling the various pages together. Let's start with the homepage.

To begin developing the pages of our application, we will create a new folder called pages inside the src directory. This folder will hold all the pages needed for our project.

For each of the pages listed below, you will need to create a corresponding file inside the src/pages folder, just as you did before with the components.

HomePage

The Home Page

The given React component, which functions as a page, represents the Home page of the application. This page is designed to provide users with an engaging and informative introduction to the application's features. It is composed of three main components: **Header**, **Hero**, and **CreateGame**.

  1. Hero Component: Positioned after the header, the **Hero** component serves as a prominent visual element. It features captivating texts and call-to-action buttons for triggering the create game modal and for easy navigation to displaying games the user is involved in.

  2. CreateGame Component: commands attention post-hero section, presenting an immersive interface for effortlessly crafting new games. This dynamic modal-like feature encompasses user-friendly form fields, interactive buttons, and elements that streamline the creation of captivating game environments.

The **Home** component orchestrates the arrangement of these components in a sequential manner within a **<div>** container. This ensures a cohesive and structured layout for the Home page, where users can quickly access navigation options, engage with compelling content in the hero section, and seamlessly create new games through the **CreateGame** interface.
In essence, the Home page presents a captivating introduction to the application, inviting users to explore its features and participate in game creation. It strategically utilizes the **Header**, **Hero**, and **CreateGame** components to achieve an engaging and user-centric presentation.

import React from 'react'
import { Header, Hero, CreateGame } from '../components'
const Home = () => {
return (
<div>
<Header />
<Hero />
<CreateGame />
</div>
)
}
export default Home
view raw Home.jsx hosted with ❤ by GitHub

MyGames Page

My Games Page

The "My Games" Page offers users a centralized space to manage and monitor their ongoing gaming activities. It seamlessly integrates three pivotal components:

  1. GameList Component: This element presents users with a structured view of the games they are currently participating in.

  2. InviteModal Component: Users can easily send out invitations to other participants using this feature, streamlining collaboration within games.

The **MyGames** component expertly orchestrates these components, ensuring a smooth and user-friendly experience. Real-time updates are facilitated through data synchronization powered by **useGlobalState**, ensuring users have access to the most current information about their dynamic gaming landscape.

import React, { useEffect } from 'react'
import { Header, GameList, InviteModal } from '../components'
import { getMyGames } from '../services/blockchain'
import { useGlobalState } from '../store'
const MyGames = () => {
const [myGames] = useGlobalState('myGames')
const fetchGameData = async () => {
await getMyGames()
}
useEffect(() => {
fetchGameData()
}, [])
return (
<div>
<Header />
<GameList games={myGames} />
<InviteModal />
</div>
)
}
export default MyGames
view raw MyGames.jsx hosted with ❤ by GitHub

Invitations Page

The Invitation Page

The "Invitations" Page offers users a focused perspective on their received invitations. This page includes:

  1. InvitationList Component: Designed to exhibit all invitations that have been received.

The **Invitations** component optimizes the process by utilizing the **fetchInvitations** function to retrieve invitation data. This approach ensures that users have a straightforward and comprehensive overview of their pending invitations, facilitating efficient decision-making.

import { useEffect } from 'react'
import { getInvitations } from "../services/blockchain"
import { useGlobalState } from '../store'
import { InvitationList, Header } from '../components'
const Invitations = () => {
const [invitations] = useGlobalState('invitations')
const fetchInvitations = async () => {
await getInvitations()
}
useEffect(() => {
fetchInvitations()
}, [])
return (
<div>
<Header />
<InvitationList invitations={invitations} />
</div>
);
}
export default Invitations
view raw Invitations.jsx hosted with ❤ by GitHub

GamePlay Page

Game Play

Game Play Result and Payout

The "GamePlay" Page sets the stage for an immersive gaming experience, bringing together:

  1. Game Component: A pivotal element that presents the ongoing game environment, catering to active player engagement.

  2. GameResult Component: Unveils the culmination of the game, displaying scores and outcomes.

The GamePlay page offers an enriching encounter, where users can actively participate in gameplay, receive results through the GameResult component, and seamlessly communicate with others for an engaging and holistic gaming adventure.

import React, { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'
import { Header, Game, Chat } from '../components'
import { getGame, getScores } from '../services/blockchain'
import { setGlobalState, useGlobalState } from '../store'
import { getGroup } from '../services/chat'
import GameResult from '../components/GameResult'
const GamePlay = () => {
const { id } = useParams()
const [game] = useGlobalState('game')
const [scores] = useGlobalState('scores')
const [connectedAccount] = useGlobalState('connectedAccount')
const [loaded, setLoaded] = useState(false)
useEffect(() => {
const fetchData = async () => {
await getGame(id)
await getScores(id)
setLoaded(true)
const GROUP = await getGroup(`guid_${id}`)
setGlobalState('group', GROUP)
}
fetchData()
}, [id])
return (
loaded && (
<>
<Header />
<Game
game={game}
isPlayed={scores.some(
(score) => score.played && score.player == connectedAccount
)}
/>
<GameResult game={game} scores={scores} />
<Chat gid={id} />
</>
)
)
}
export default GamePlay
view raw GamePlay.jsx hosted with ❤ by GitHub

The Blockchain Service

Step into the PlayToEarn realm armed with the robust PlayToEarn Blockchain Service script. Let's dive into its core functionalities:

  1. Wallet Connectivity: Seamlessly manage wallet connections with isWalletConnected and initiate connections using connectWallet. Track and respond to connected account changes for a frictionless experience.

  2. Game Creation: With createGame, establish new gaming experiences. This function communicates with the blockchain contract to create games.

  3. Invitation Management: invitePlayer, acceptInvitation, and rejectInvitation offer a dynamic invitation journey. These functions communicate with the blockchain contract, enabling seamless invitations, responses, and management.

  4. Gameplay Tracking: recordScore keeps track of gameplay by recording scores. It ensures that scores are updated through transactions on the blockchain.

  5. Payouts: Utilize payout to trigger the rewarding moment. This function facilitates smooth payouts by interacting with the blockchain contract.

  6. Data Exploration: getGames, getMyGames, getGame, getInvitations, and getScores are your guides to accessing diverse game-related data. These functions retrieve and structure data from the blockchain contract, offering insights into your PlayToEarn journey.

  7. Error Management: In case of unexpected issues, reportError comes to your aid. It logs errors encountered during interactions, ensuring you stay informed.

With utility functions like toWei and fromWei for unit conversions, and a setup that ensures continuous data synchronization, this PlayToEarn Blockchain Service script empowers your journey.

To integrate this script, create a blockchain.jsx file within the services folder of your src directory. Paste the provided code, and you're primed to immerse yourself in the thrilling world of PlayToEarn, backed by the prowess of blockchain technology.

import { setGlobalState } from '../store'
import abi from '../abis/src/contracts/PlayToEarn.sol/PlayToEarn.json'
import address from '../abis/contractAddress.json'
import { ethers } from 'ethers'
import { logOutWithCometChat } from './chat'
const { ethereum } = window
const ContractAddress = address.address
const ContractAbi = abi.abi
let tx
const toWei = (num) => ethers.utils.parseEther(num.toString())
const fromWei = (num) => ethers.utils.formatEther(num)
const getEthereumContract = async () => {
const accounts = await ethereum.request({ method: 'eth_accounts' })
const provider = accounts[0]
? new ethers.providers.Web3Provider(ethereum)
: new ethers.providers.JsonRpcProvider(process.env.REACT_APP_RPC_URL)
const wallet = accounts[0] ? null : ethers.Wallet.createRandom()
const signer = provider.getSigner(accounts[0] ? undefined : wallet.address)
const contract = new ethers.Contract(ContractAddress, ContractAbi, signer)
return contract
}
const isWalletConnected = async () => {
try {
if (!ethereum) {
reportError('Please install Metamask')
return Promise.reject(new Error('Metamask not installed'))
}
const accounts = await ethereum.request({ method: 'eth_accounts' })
if (accounts.length) {
setGlobalState('connectedAccount', accounts[0])
} else {
console.log('No accounts found.')
}
window.ethereum.on('chainChanged', (chainId) => {
window.location.reload()
})
window.ethereum.on('accountsChanged', async () => {
setGlobalState('connectedAccount', accounts[0])
await loadData()
await isWalletConnected()
await logOutWithCometChat()
})
if (accounts.length) {
setGlobalState('connectedAccount', accounts[0])
} else {
setGlobalState('connectedAccount', '')
console.log('No accounts found')
}
} catch (error) {
reportError(error)
}
}
const connectWallet = async () => {
try {
if (!ethereum) return alert('Please install Metamask')
const accounts = await ethereum.request({ method: 'eth_requestAccounts' })
setGlobalState('connectedAccount', accounts[0])
} catch (error) {
reportError(error)
}
}
const createGame = async ({
title,
description,
participants,
winners,
challenges,
starts,
ends,
stake,
}) => {
if (!ethereum) return alert('Please install Metamask')
return new Promise(async (resolve, reject) => {
try {
const contract = await getEthereumContract()
tx = await contract.createGame(
title,
description,
participants,
winners,
challenges,
starts,
ends,
{
value: toWei(stake),
}
)
await tx.wait()
await getMyGames()
resolve(tx)
} catch (error) {
reportError(error)
reject(error)
}
})
}
const invitePlayer = async (player, gameId) => {
if (!ethereum) return alert('Please install Metamask')
return new Promise(async (resolve, reject) => {
try {
const contract = await getEthereumContract()
tx = await contract.invitePlayer(player, gameId)
await tx.wait()
resolve(tx)
} catch (err) {
reportError(err)
reject(err)
}
})
}
const acceptInvitation = async (gameId, stake) => {
if (!ethereum) return alert('Please install Metamask')
return new Promise(async (resolve, reject) => {
try {
const contract = await getEthereumContract()
tx = await contract.acceptInvitation(gameId, {
value: toWei(stake),
})
await tx.wait()
await getInvitations()
resolve(tx)
} catch (err) {
reportError(err)
reject(err)
}
})
}
const rejectInvitation = async (gameId) => {
if (!ethereum) return alert('Please install Metamask')
return new Promise(async (resolve, reject) => {
try {
const contract = await getEthereumContract()
tx = await contract.rejectInvitation(gameId)
await tx.wait()
await getInvitations()
resolve(tx)
} catch (err) {
reportError(err)
reject(err)
}
})
}
const recordScore = async (gameId, score) => {
if (!ethereum) return alert('Please install Metamask')
return new Promise(async (resolve, reject) => {
try {
const contract = await getEthereumContract()
tx = await contract.recordScore(gameId, score)
await tx.wait()
await getGame(gameId)
resolve(tx)
} catch (err) {
reportError(err)
reject(err)
}
})
}
const payout = async (gameId) => {
if (!ethereum) return alert('Please install metamask')
return new Promise(async (resolve, reject) => {
try {
const contract = await getEthereumContract()
tx = await contract.payout(gameId)
await tx.wait()
await getGame(gameId)
resolve(tx)
} catch (err) {
reportError(err)
reject(err)
}
})
}
const loadData = async () => {
await getMyGames()
await getInvitations()
}
const getGames = async () => {
try {
if (!ethereum) return alert('Please install Metamask')
const contract = await getEthereumContract()
const games = await contract.getGames()
setGlobalState('games', structuredGames(games))
} catch (err) {
reportError(err)
}
}
const getGame = async (id) => {
try {
if (!ethereum) return alert('Please install Metamask')
const contract = await getEthereumContract()
const game = await contract.getGame(id)
setGlobalState('game', structuredGames([game])[0])
} catch (err) {
reportError(err)
}
}
const getInvitations = async () => {
try {
if (!ethereum) return alert('Please install Metamask')
const contract = await getEthereumContract()
const invitations = await contract.getInvitations()
setGlobalState('invitations', structuredInvitations(invitations))
} catch (err) {
reportError(err)
}
}
const getScores = async (id) => {
try {
if (!ethereum) return alert('Please install Metamask')
const contract = await getEthereumContract()
const scores = await contract.getScores(id)
setGlobalState('scores', structuredPlayersScore(scores))
} catch (err) {
reportError(err)
}
}
const getMyGames = async () => {
try {
if (!ethereum) return alert('Please install Metamask')
const contract = await getEthereumContract()
const games = await contract.getMyGames()
setGlobalState('myGames', structuredGames(games))
} catch (err) {
reportError(err)
}
}
const structuredGames = (games) =>
games
.map((game) => ({
id: game.id.toNumber(),
title: game.title,
description: game.description,
owner: game.owner.toLowerCase(),
participants: game.participants.toNumber(),
challenges: game.challenges.toNumber(),
numberOfWinners: game.numberOfWinners.toNumber(),
plays: game.plays.toNumber(),
acceptees: game.acceptees.toNumber(),
stake: fromWei(game.stake),
startDate: game.startDate.toNumber(),
endDate: game.endDate.toNumber(),
timestamp: game.timestamp.toNumber(),
deleted: game.deleted,
paidOut: game.paidOut,
}))
.sort((a, b) => b.timestamp - a.timestamp)
const structuredPlayersScore = (playersScore) =>
playersScore
.map((playerScore) => ({
gameId: playerScore.gameId.toNumber(),
player: playerScore.player.toLowerCase(),
score: playerScore.score.toNumber(),
played: playerScore.played,
}))
.sort((a, b) => {
if (a.played !== b.played) {
return a.played ? -1 : 1
} else {
return a.score - b.score
}
})
const structuredInvitations = (invitations) =>
invitations.map((invitation) => ({
gameId: invitation.gameId.toNumber(),
account: invitation.account.toLowerCase(),
responded: invitation.responded,
accepted: invitation.accepted,
title: invitation.title,
stake: fromWei(invitation.stake),
}))
export {
connectWallet,
isWalletConnected,
createGame,
invitePlayer,
acceptInvitation,
rejectInvitation,
recordScore,
payout,
getGames,
getMyGames,
getGame,
getInvitations,
getScores,
loadData,
}
view raw blockchain.jsx hosted with ❤ by GitHub

Please ensure that you update the environment variables to look like this:

REACT_APP_COMETCHAT_APP_ID=****************
REACT_APP_COMETCHAT_AUTH_KEY=******************************
REACT_APP_COMETCHAT_REGION=**
REACT_APP_RPC_URL=http://127.0.0.1:8545
Enter fullscreen mode Exit fullscreen mode

The Chat Service

The CometChat service script provides various functions and utilities for integrating CometChat into the application. Here's an overview of the key functionalities:

  1. Initialization: Begin by firing up the CometChat engine with initCometChat. This function gets the SDK ready, ensuring seamless communication.

  2. User Authentication: Seamlessly authenticate users with CometChat using loginWithCometChat and signUpWithCometChat. These functions make user logins and sign-ups effortless, returning user objects as Promises.

  3. User Logout: Wave goodbye to complications with logOutWithCometChat. It gracefully logs out users from CometChat and clears their state from the global context.

  4. User Authentication State: Stay updated on user authentication states with checkAuthState. It fetches the currently logged-in user from CometChat, keeping your app's user data synchronized.

  5. Messaging Magic: Transform messaging into a breeze! getMessages fetches previous messages, sendMessage sends text messages, and listenForMessage tunes in to incoming text messages, ensuring dynamic conversations.

  6. Conversations at Your Fingertips: Effortlessly retrieve conversations with getConversations. It effortlessly fetches user conversations, enhancing real-time engagement.

  7. Group Dynamics: For more robust interaction, enjoy functionalities like createNewGroup to initiate a new group, getGroup to fetch group details, and joinGroup to become part of an existing group.

The service script provides a set of functions to initialize CometChat, handle user authentication and logout, fetch and send messages, and manage conversation data. These functionalities enable real-time messaging and chat features in the application using the CometChat SDK.

Continuing inside the services folder, create a new file called chat.jsx. Once you have created the file, you can copy and paste the code below into it.

import { CometChat } from '@cometchat-pro/chat'
import { setGlobalState } from '../store'
const COMETCHAT_CONSTANTS = {
APP_ID: process.env.REACT_APP_COMET_CHAT_APP_ID,
REGION: process.env.REACT_APP_COMET_CHAT_REGION,
AUTH_KEY: process.env.REACT_APP_COMET_CHAT_AUTH_KEY,
}
const initCometChat = async () => {
const appID = COMETCHAT_CONSTANTS.APP_ID
const region = COMETCHAT_CONSTANTS.REGION
const appSetting = new CometChat.AppSettingsBuilder()
.subscribePresenceForAllUsers()
.setRegion(region)
.build()
await CometChat.init(appID, appSetting)
.then(() => console.log('Initialization completed successfully'))
.catch((error) => console.log(error))
}
const loginWithCometChat = async (UID) => {
const authKey = COMETCHAT_CONSTANTS.AUTH_KEY
return new Promise(async (resolve, reject) => {
await CometChat.login(UID, authKey)
.then((user) => resolve(user))
.catch((error) => reject(error))
})
}
const signUpWithCometChat = async (UID) => {
const authKey = COMETCHAT_CONSTANTS.AUTH_KEY
const user = new CometChat.User(UID)
user.setName(UID)
return new Promise(async (resolve, reject) => {
await CometChat.createUser(user, authKey)
.then((user) => resolve(user))
.catch((error) => reject(error))
})
}
const logOutWithCometChat = async () => {
return new Promise(async (resolve, reject) => {
await CometChat.logout()
.then(() => {
setGlobalState('currentUser', null)
resolve()
})
.catch(() => reject())
})
}
const checkAuthState = async () => {
return new Promise(async (resolve, reject) => {
await CometChat.getLoggedinUser()
.then((user) => {
setGlobalState('currentUser', user)
resolve(user)
})
.catch((error) => reject(error))
})
}
const getMessages = async (GUID) => {
const limit = 30
const messagesRequest = new CometChat.MessagesRequestBuilder()
.setGUID(GUID)
.setLimit(limit)
.build()
return new Promise(async (resolve, reject) => {
await messagesRequest
.fetchPrevious()
.then((messages) => resolve(messages.filter((msg) => msg.type == 'text')))
.catch((error) => reject(error))
})
}
const getConversations = async () => {
const limit = 30
const conversationsRequest = new CometChat.ConversationsRequestBuilder()
.setLimit(limit)
.build()
return new Promise(async (resolve, reject) => {
await conversationsRequest
.fetchNext()
.then((conversations) => resolve(conversations))
.catch((error) => reject(error))
})
}
const sendMessage = async (receiverID, messageText) => {
const receiverType = CometChat.RECEIVER_TYPE.GROUP
const textMessage = new CometChat.TextMessage(
receiverID,
messageText,
receiverType
)
return new Promise(async (resolve, reject) => {
await CometChat.sendMessage(textMessage)
.then((message) => resolve(message))
.catch((error) => reject(error))
})
}
const listenForMessage = async (listenerID) => {
return new Promise(async (resolve, reject) => {
CometChat.addMessageListener(
listenerID,
new CometChat.MessageListener({
onTextMessageReceived: (message) => resolve(message),
})
)
})
}
const createNewGroup = async (GUID, groupName) => {
const groupType = CometChat.GROUP_TYPE.PUBLIC
const password = ''
const group = new CometChat.Group(GUID, groupName, groupType, password)
return new Promise(async (resolve, reject) => {
await CometChat.createGroup(group)
.then((group) => resolve(group))
.catch((error) => reject(error))
})
}
const getGroup = async (GUID) => {
return new Promise(async (resolve, reject) => {
await CometChat.getGroup(GUID)
.then((group) => resolve(group))
.catch((error) => reject(error))
})
}
const joinGroup = async (GUID) => {
const groupType = CometChat.GROUP_TYPE.PUBLIC
const password = ''
return new Promise(async (resolve, reject) => {
await CometChat.joinGroup(GUID, groupType, password)
.then((group) => resolve(group))
.catch((error) => reject(error))
})
}
export {
initCometChat,
loginWithCometChat,
signUpWithCometChat,
logOutWithCometChat,
getMessages,
sendMessage,
checkAuthState,
listenForMessage,
getConversations,
createNewGroup,
getGroup,
joinGroup,
}
view raw chat.jsx hosted with ❤ by GitHub

Excellent! Now, let's work on the store file, which serves as a state management library.

The Store File

The store service provides a centralized state management solution using the [react-hooks-global-state](https://www.npmjs.com/package/react-hooks-global-state) library. It offers functions for setting, getting, and using global state variables within the application.

  1. Global State Management*:* The createGlobalState function is used to create global state variables, along with functions for setting, getting, and using the state values. These global state variables are accessible throughout the application.

  2. Global State Variables*:* The code defines various global state variables, including connectedAccount, currentUser, resultModal, createModal, chatModal, inviteModal, games, game, group, messages, invitations, scores, and myGames. These variables store different types of data relevant to the application.

  3. Utility Functions*:*

    • truncate: This function shortens text to a specified length while ensuring readability. It takes a text input, start and end characters to retain, and a maximum length. If the text exceeds the maximum length, it trims the middle portion and adds ellipsis to indicate truncation.
- `formatDate`: This function converts a timestamp into a formatted date string. It uses the `Date` object to extract year, month, and day information and then formats it accordingly.

- `timestampToDate`: This function converts a timestamp into a formatted date and time string. It extends the `formatDate` function by also including the hour and minute information.
Enter fullscreen mode Exit fullscreen mode

To use this service, you will need to create a new folder called store inside the src directory of your project. Inside the store folder, you will need to create a new file called index.jsx. Once you have created the file, you can copy and paste the code below into it.

import { createGlobalState } from 'react-hooks-global-state'
const { setGlobalState, useGlobalState, getGlobalState } = createGlobalState({
connectedAccount: '',
currentUser: null,
resultModal: 'scale-0',
createModal: 'scale-0',
chatModal: 'scale-0',
inviteModal: 'scale-0',
games: [],
game: null,
group: null,
messages: [],
invitations: [],
scores: [],
myGames: [],
})
const truncate = (text, startChars, endChars, maxLength) => {
if (text.length > maxLength) {
let start = text.substring(0, startChars)
let end = text.substring(text.length - endChars, text.length)
while (start.length + end.length < maxLength) {
start = start + '.'
}
return start + end
}
return text
}
const formatDate = (timestamp) => {
const date = new Date(timestamp)
const options = {
year: 'numeric',
month: 'long',
day: 'numeric',
}
return date.toLocaleDateString(undefined, options)
}
const timestampToDate = (timestamp) => {
const date = new Date(timestamp)
const options = {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
}
return date.toLocaleDateString('en-US', options)
}
export {
setGlobalState,
useGlobalState,
getGlobalState,
truncate,
formatDate,
timestampToDate,
}
view raw index.jsx hosted with ❤ by GitHub

The Index files

The index.jsx file is the entry point for the application. It initializes the CometChat service, sets up dependencies, and renders the React application using the App component within a BrowserRouter. It creates a root element for rendering and sets up the necessary configurations for the application to start running.

To use this code, you will need to replace the code below inside of the index.jsx and index.css files in the src folder of your project.

@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700&display=swap');
* html {
padding: 0;
margin: 0;
box-sizing: border-box;
}
body {
margin: 0;
font-family: 'Open Sans', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@tailwind base;
@tailwind components;
@tailwind utilities;
view raw index.css hosted with ❤ by GitHub
import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import 'react-toastify/dist/ReactToastify.css'
import './index.css'
import App from './App'
import { initCometChat } from './services/chat'
const root = ReactDOM.createRoot(document.getElementById('root'))
initCometChat().then(() => {
root.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
)
})
view raw index.jsx hosted with ❤ by GitHub

Now you are officially done with the build, just execute **yarn start** to have the application running on the browser.

Congratulations on successfully creating a Web3 Play-To-Earn Money Dapp using React, Solidity, and integrating CometChat for real-time communication! This accomplishment showcases your ability to combine cutting-edge technologies to develop an innovative and engaging application. By leveraging React for the frontend, Solidity for the smart contracts, and CometChat for seamless chatting features, you've demonstrated a versatile skill set in both blockchain development and interactive user experiences.

Here is a link to the CometChat website where you can learn more about the SDK and how to get started.

For more web3 resources, check out this video that teaches how to create a decentralized app by building a web3 lottery dapp, I recommend that you it.

The video provides a hands-on tutorial on how to build a lottery dapp using NextJs, Tailwind CSS, and Solidity.

Conclusion

Crafting a Web3 Play-To-Earn Money Dapp with React, Solidity, and CometChat" is a comprehensive guide that delves into the amalgamation of React, blockchain, and real-time chat to create an extraordinary Play-To-Earn Money Dapp. This project harnesses the prowess of smart contracts to ensure secure transactions and boasts an intuitive user interface. By seamlessly integrating CometChat, it elevates user interaction through dynamic real-time communication.

This tutorial serves as a gateway to the future, showcasing the incredible potential of web3 development in revolutionizing the concept of play-to-earn applications. Through meticulous testing, the smart contracts are fortified to provide reliability and trustworthiness. As developers embark on this transformative journey, they're invited to explore the expansive possibilities that blockchain technology unveils, setting the stage for a new era of interactive and lucrative gaming experiences.

For further learning, we recommends subscribing to our YouTube channel and visiting our website for additional resources.

Till next time all the best!

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
Twitter: Follow
LinkedIn: Connect
GitHub: Explore
Website: Visit

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read full post →

Top comments (1)

Collapse
 
daltonic profile image
Gospel Darlington

Drop your thoughts here at the comment section.