DEV Community

Cover image for How to Build a Web3 NFT Cross-Breeding Dapp with React, Solidity, and CometChat
Gospel Darlington
Gospel Darlington

Posted on

4

How to Build a Web3 NFT Cross-Breeding Dapp with React, Solidity, and CometChat

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

Breeding Two NFTs

One-on-One Chat with CometChat

Are you a blockchain enthusiast looking to explore the exciting world of non-fungible tokens (NFTs) and their unique capabilities? Are you interested in building a web3 application that allows users to cross-breed NFTs and create new, one-of-a-kind digital assets? If so, you've come to the right place.

In this tutorial, we will walk you through the process of building a web3 NFT cross-breeding DApp using React, Solidity, and CometChat. React will serve as our front-end framework, providing a robust and interactive user interface. Solidity, the programming language for Ethereum smart contracts, will enable us to define the logic and behavior of our NFT cross-breeding functionality.

Lastly, CometChat will empower us to integrate real-time chat functionality into our DApp, allowing users to interact and communicate with each other seamlessly.

By the end of this tutorial, you will have gained valuable insights into:

  • Developing a React application for NFT cross-breeding
  • Developing the Solidity smart contracts with both minting and breeding functionalities
  • Leveraging CometChat to incorporate real-time chat features into your DApp

Whether you are an experienced developer or just starting your journey into web3 development, this tutorial will equip you with the knowledge and skills to create your very own NFT cross-breeding DApp. So, let's embark on this exciting journey together and unlock the potential of NFT cross-breeding in the blockchain realm. Get ready to dive into the fascinating intersection of React, Solidity, and CometChat – let's begin!

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 dappBreeds
cd dappBreeds
Enter fullscreen mode Exit fullscreen mode

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

{
"name": "dappbreeds",
"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.12",
"@headlessui/react": "^1.7.15",
"@nomiclabs/hardhat-ethers": "^2.1.0",
"@nomiclabs/hardhat-waffle": "^2.0.3",
"ethereum-waffle": "^3.4.4",
"ethers": "^5.6.9",
"hardhat": "^2.10.1",
"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-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 DappBreeds.

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

STEP 5:
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 new files named DappBreeds.sol and Base64.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 DappBreeds.sol and Base64.sol files, which will serve as the foundation for implementing the logic of the smart contract.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/// @title Base64
/// @author Brecht Devos - <brecht@loopring.org>
/// @notice Provides a function for encoding some bytes in base64
library Base64 {
string internal constant TABLE =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
function encode(bytes memory data) internal pure returns (string memory) {
if (data.length == 0) return "";
// load the table into memory
string memory table = TABLE;
// multiply by 4/3 rounded up
uint256 encodedLen = 4 * ((data.length + 2) / 3);
// add some extra buffer at the end required for the writing
string memory result = new string(encodedLen + 32);
assembly {
// set the actual output length
mstore(result, encodedLen)
// prepare the lookup table
let tablePtr := add(table, 1)
// input ptr
let dataPtr := data
let endPtr := add(dataPtr, mload(data))
// result ptr, jump over length
let resultPtr := add(result, 32)
// run over the input, 3 bytes at a time
for {
} lt(dataPtr, endPtr) {
} {
dataPtr := add(dataPtr, 3)
// read 3 bytes
let input := mload(dataPtr)
// write 4 characters
mstore(
resultPtr,
shl(248, mload(add(tablePtr, and(shr(18, input), 0x3F))))
)
resultPtr := add(resultPtr, 1)
mstore(
resultPtr,
shl(248, mload(add(tablePtr, and(shr(12, input), 0x3F))))
)
resultPtr := add(resultPtr, 1)
mstore(
resultPtr,
shl(248, mload(add(tablePtr, and(shr(6, input), 0x3F))))
)
resultPtr := add(resultPtr, 1)
mstore(
resultPtr,
shl(248, mload(add(tablePtr, and(input, 0x3F))))
)
resultPtr := add(resultPtr, 1)
}
// padding with '='
switch mod(mload(data), 3)
case 1 {
mstore(sub(resultPtr, 2), shl(240, 0x3d3d))
}
case 2 {
mstore(sub(resultPtr, 1), shl(248, 0x3d))
}
}
return result;
}
}
view raw Base64.sol hosted with ❤ by GitHub
//SPDX-License-Identifier:MIT
pragma solidity >=0.7.0 <0.9.0;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "./Base64.sol";
contract DappBreed is ERC721, ERC721URIStorage, Ownable, ReentrancyGuard {
using Strings for uint256;
using Counters for Counters.Counter;
Counters.Counter private _tokenIdCounter;
struct TraitStruct {
string name;
string description;
string weapon;
string image;
string environment;
uint256 rarity;
bool breeded;
uint256[] parents;
}
struct MintStruct {
uint256 id;
address owner;
uint256 mintCost;
uint256 timestamp;
TraitStruct traits;
}
string public baseURI;
uint256 public maxSupply;
string public baseExtension = ".json";
string public imageExtension = ".webp";
uint256 public mintCost = 0.005 ether;
uint256 public totalBalance;
mapping(uint256 => MintStruct) minted;
mapping(uint256 => bool) tokenIdExist;
string[] weapons = [
"Stick",
"Knife",
"Blade",
"Club",
"Ax",
"Sword",
"Spear",
"Halberd"
];
string[] environments = [
"Space",
"Sky",
"Deserts",
"Forests",
"Grasslands",
"Mountains",
"Oceans",
"Rainforests"
];
uint256[] rarities = new uint256[](5);
constructor(
string memory _name,
string memory _symbol,
string memory _BaseURI,
uint256 _maxSupply
) ERC721(_name, _symbol) {
baseURI = _BaseURI;
maxSupply = _maxSupply;
}
function _baseURI() internal view virtual override returns (string memory) {
return baseURI;
}
function mintNft() public payable nonReentrant {
require(
_tokenIdCounter.current() <= maxSupply,
"Out of tokens, check back later"
);
require(
msg.value > 0 ether && msg.value >= mintCost,
"Insufficient fund for minting"
);
_tokenIdCounter.increment();
uint256 _tokenId = _tokenIdCounter.current();
require(_performMinting(_tokenId), "minting unsuccessful");
TraitStruct memory nft;
nft.name = string(
abi.encodePacked(symbol(), " #", _tokenId.toString())
);
nft
.description = "This is a minted AI generated artworks available for your use.";
nft.weapon = weapons[randomNum(weapons.length, currentTime(), 0)];
nft.environment = environments[
randomNum(environments.length, currentTime(), 0)
];
nft.rarity = randomNum(rarities.length, currentTime(), 0);
nft.image = string(
abi.encodePacked(baseURI, _tokenId.toString(), imageExtension)
);
minted[_tokenId].traits = nft;
payTo(owner(), msg.value);
}
function breedNft(
uint256 _fatherTokenId,
uint256 _motherTokenId
) public payable nonReentrant {
require(tokenIdExist[_fatherTokenId], "Father does not exist");
require(tokenIdExist[_motherTokenId], "Mother does not exist");
require(
_tokenIdCounter.current() <= maxSupply,
"Out of tokens, check back later"
);
require(
msg.value > 0 ether && msg.value >= mintCost,
"Insufficient fund for minting"
);
_tokenIdCounter.increment();
uint256 _tokenId = _tokenIdCounter.current();
require(_performMinting(_tokenId), "minting unsuccessful");
TraitStruct memory nft;
nft.name = string(
abi.encodePacked(symbol(), " #", _tokenId.toString())
);
nft
.description = "This is an inherited AI generated artworks available for your use.";
nft.weapon = minted[_fatherTokenId].traits.weapon;
nft.environment = minted[_motherTokenId].traits.environment;
nft.rarity = randomNum(rarities.length, block.timestamp, 0);
nft.breeded = true;
nft.parents = new uint256[](2);
nft.parents[0] = _fatherTokenId;
nft.parents[1] = _motherTokenId;
nft.image = string(
abi.encodePacked(baseURI, _tokenId.toString(), imageExtension)
);
minted[_tokenId].traits = nft;
payTo(owner(), msg.value);
}
function getAllNfts() public view returns (MintStruct[] memory Minted) {
Minted = new MintStruct[](_tokenIdCounter.current());
for (uint256 i = 0; i < _tokenIdCounter.current(); i++) {
Minted[i] = minted[i + 1];
}
}
function getMintedNfts() public view returns (MintStruct[] memory Minted) {
uint256 available;
for (uint256 i = 0; i < _tokenIdCounter.current(); i++) {
if (!minted[i + 1].traits.breeded) available++;
}
Minted = new MintStruct[](available);
uint256 index;
for (uint256 i = 0; i < _tokenIdCounter.current(); i++) {
if (!minted[i + 1].traits.breeded) Minted[index++] = minted[i + 1];
}
}
function getBreededNfts() public view returns (MintStruct[] memory Minted) {
uint256 available;
for (uint256 i = 0; i < _tokenIdCounter.current(); i++) {
if (minted[i + 1].traits.breeded) available++;
}
Minted = new MintStruct[](available);
uint256 index;
for (uint256 i = 0; i < _tokenIdCounter.current(); i++) {
if (minted[i + 1].traits.breeded) Minted[index++] = minted[i + 1];
}
}
function getMyNfts() public view returns (MintStruct[] memory Minted) {
uint256 available;
for (uint256 i = 0; i < _tokenIdCounter.current(); i++) {
if (minted[i + 1].owner == msg.sender) available++;
}
Minted = new MintStruct[](available);
uint256 index;
for (uint256 i = 0; i < _tokenIdCounter.current(); i++) {
if (minted[i + 1].owner == msg.sender)
Minted[index++] = minted[i + 1];
}
}
function getParentsOf(
uint256 _tokenId
) public view returns (MintStruct[] memory Minted) {
if (!minted[_tokenId].traits.breeded) {
Minted = new MintStruct[](0);
return Minted;
}
Minted = new MintStruct[](minted[_tokenId].traits.parents.length);
uint256 index;
for (uint256 i = 0; i < _tokenIdCounter.current(); i++) {
if (
minted[i + 1].id == minted[_tokenId].traits.parents[0] ||
minted[i + 1].id == minted[_tokenId].traits.parents[1]
) {
Minted[index++] = minted[i + 1];
}
}
}
function getNft(uint256 _tokenId) public view returns (MintStruct memory) {
return minted[_tokenId];
}
function setBaseURI(string memory _newBaseURI) public onlyOwner {
baseURI = _newBaseURI;
}
function _performMinting(uint256 _tokenId) internal returns (bool) {
_safeMint(msg.sender, _tokenId);
_setTokenURI(_tokenId, tokenURI(_tokenId));
MintStruct memory mint;
mint.id = _tokenId;
mint.owner = msg.sender;
mint.mintCost = msg.value;
mint.timestamp = currentTime();
minted[_tokenId] = mint;
tokenIdExist[_tokenId] = true;
return true;
}
function _burn(
uint256 _tokenId
) internal override(ERC721, ERC721URIStorage) {
super._burn(_tokenId);
}
function tokenURI(
uint256 _tokenId
) public view override(ERC721, ERC721URIStorage) returns (string memory) {
require(
_exists(_tokenId),
"ERC721Metadata: URI query for nonexistent token"
);
return buildMetadata(_tokenId);
}
function buildMetadata(
uint256 _tokenId
) internal view returns (string memory) {
TraitStruct memory traits = minted[_tokenId].traits;
uint256 timestamp = currentTime();
bytes memory attributesJson = buildAttributesJson(
traits.environment,
traits.weapon,
traits.rarity,
timestamp
);
return
string(
abi.encodePacked(
"data:application/json;base64,",
Base64.encode(
bytes(
abi.encodePacked(
'{"id":"',
_tokenId.toString(),
'","name":"',
traits.name,
'","description":"',
traits.description,
'","price":"',
mintCost.toString(),
'","image":"',
traits.image,
'","attributes":',
attributesJson,
"}"
)
)
)
)
);
}
function buildAttributesJson(
string memory _environment,
string memory _weapon,
uint256 _rarity,
uint256 _timestamp
) internal pure returns (bytes memory) {
return
abi.encodePacked(
'[{"trait_type":"Environment","value":"',
_environment,
'"},',
'{"trait_type":"Weapon","value":"',
_weapon,
'"},',
'{"trait_type":"Rarity","value":"',
_rarity.toString(),
'"},',
'{"display_type":"date","trait_type":"Created","value":"',
_timestamp.toString(),
'"}]'
);
}
function randomNum(
uint256 _mod,
uint256 _seed,
uint256 _salt
) internal view returns (uint256) {
uint256 num = uint256(
keccak256(
abi.encodePacked(block.timestamp, msg.sender, _seed, _salt)
)
) % _mod;
return num;
}
function currentTime() internal view returns (uint256) {
uint256 newNum = (block.timestamp * 1000) + 1000;
return newNum;
}
function payTo(address to, uint256 amount) internal {
(bool success, ) = payable(to).call{value: amount}("");
require(success);
}
function supportsInterface(
bytes4 interfaceId
) public view virtual override(ERC721, ERC721URIStorage) returns (bool) {
return super.supportsInterface(interfaceId);
}
}
view raw DappBreed.sol hosted with ❤ by GitHub

The DappBreed smart contract is designed to facilitate the creation and breeding of AI-generated artworks as non-fungible tokens (NFTs). Let's provide an overview of its key components and functions:

  1. Contract Inheritance and Dependencies:

    • DappBreed inherits from several other contracts, including ERC721, ERC721URIStorage, Ownable, and ReentrancyGuard.
    • It also imports libraries such as Counters and Base64, as well as contracts from the OpenZeppelin library.
  2. Structs:

    • TraitStruct: Represents the traits of an AI-generated artwork, including name, description, weapon, image URI, environment, rarity, breeding status, and parent tokens.
    • MintStruct: Contains information about a minted NFT, such as its ID, owner, minting cost, timestamp, and associated TraitStruct.
  3. State Variables:

    • baseURI: Stores the base URI for the metadata of NFTs.
    • maxSupply: Indicates the maximum number of NFTs that can be minted.
    • baseExtension and imageExtension: Define the file extensions for the metadata and image files.
    • mintCost: Specifies the cost in Ether for minting an NFT.
    • totalBalance: Tracks the total balance (in Ether) accumulated from minting fees.
  4. Mappings:

    • minted: Maps the token ID to its associated MintStruct, storing information about each minted NFT.
    • tokenIdExist: Tracks the existence of a token ID.
  5. Arrays:

    • weapons: Contains a predefined list of weapon names used for generating AI artwork traits.
    • environments: Holds a predefined list of environment names for AI artwork traits.
    • rarities: An array to represent the rarity levels of AI artworks.
  6. Constructor:

    • Initializes the contract with a name, symbol, baseURI, and maxSupply for the NFTs.
  7. Minting Functions:

    • mintNft: Allows users to mint a new NFT by paying the required mintCost.
    • breedNft: Enables users to breed two existing NFTs to create a new one, paying the mintCost.
    • _performMinting: Internal function to perform the minting process, creating the NFT and updating relevant data.
  8. View Functions:

    • getAllNfts: Returns an array of all minted NFTs.
    • getMintedNfts: Retrieves an array of NFTs that have not been bred.
    • getBreededNfts: Returns an array of NFTs that have been bred.
    • getMyNfts: Retrieves an array of NFTs owned by the caller.
    • getParentsOf: Returns an array of parent NFTs for a given token ID.
    • getNft: Retrieves the MintStruct information for a specific token ID.
    • buildMetadata: Internal function to build the metadata URI for a given token ID.
  9. Administrative Functions:

    • setBaseURI: Allows the contract owner to update the baseURI for NFT metadata.
  10. Internal Utility Functions:

    • _baseURI: Overrides the baseURI function from ERC721URIStorage to return the contract's baseURI.
    • _burn: Overrides the _burn function from ERC721URIStorage to call the parent implementation.
  11. Helper Functions:

    • randomNum: Generates a random number based on input parameters.
    • currentTime: Retrieves the current timestamp.
    • payTo: Sends Ether to a specified address.
  12. Modifier:

    • nonReentrant: Prevents reentrancy attacks by enforcing a non-reentrant execution context.
  13. Inherited Functions:

    • The contract inherits various functions from the imported contracts, such as _safeMint, _setTokenURI, and supportsInterface.

The DappBreed smart contract combines the functionalities of ERC721, ERC721URIStorage, and Ownable contracts to provide a platform for minting, breeding, and managing AI-generated artworks as NFTs.

Want to get a deeper understanding of NFT developments? Purchase the "Fullstack NFT Marketplace (Course)" today! This comprehensive course will teach you what you need to know about building and deploying NFT marketplaces on Ethereum.

Fullstack NFT Marketplace (Course)

The Test Script

The DappBreed test script is designed to test the functionalities and behaviors of the DappBreed smart contract. Here's an overview of the key tests and functions present in the script:

  1. Test Setup:

    • The script initializes variables such as _maxSupply, _name, _symbol, and _baseUri for the contract deployment.
    • It sets up the deployer and two user addresses as signers for testing purposes.
  2. Contract Deployment and Minting:

    • The script deploys the DappBreed contract using the specified parameters.
    • It uses the mintNft function to mint an NFT with a value of 0.005 Ether.
    • The test checks the successful minting of the NFT and verifies the retrieved minted NFT and its ID.
  3. Minting Tests:

    • The script includes tests to confirm the successful minting of multiple NFTs.
    • It mints a second NFT using a different user address and checks the total number of minted NFTs.
  4. Breeding Tests:

    • The script sets up a scenario for NFT breeding by minting another NFT with a different user address.
    • It verifies the presence of both minted NFTs and the absence of any breeded NFTs.
    • The test then uses the breedNft function to breed the previously minted NFTs.
    • It checks the successful breeding of the NFT, the total number of owned NFTs, and the presence of breeded NFTs.
    • Additionally, the test verifies the correctness of the parents and traits of the breeded child NFT.

The DappBreed test script covers crucial aspects of the contract's functionality, including minting and breeding NFTs. It ensures that the contract behaves as expected and that the NFTs are generated and managed correctly.

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 _maxSupply = 99
const _name = 'Dapp Breeds'
const _symbol = 'DAB'
const _baseUri =
'https://ipfs.io/ipfs/QmTWbe9wDns7aqZQNCuWh5PqybGbBF91kngC5Zf8qmCoyg/'
const id = 1
const fatherTokenId = 1
const motherTokenId = 2
const childTokenId = 3
beforeEach(async () => {
const Contract = await ethers.getContractFactory('DappBreed')
;[deployer, user1, user2] = await ethers.getSigners()
contract = await Contract.deploy(_name, _symbol, _baseUri, _maxSupply)
await contract.deployed()
})
beforeEach(async () => {
await contract.mintNft({
value: toWei(0.005),
})
})
describe('Minting', () => {
it('it should confirm nft minting', async () => {
result = await contract.getMintedNfts()
expect(result).to.have.lengthOf(1)
result = await contract.getNft(id)
expect(result.id).to.be.equal(id)
})
it('it should confirm second mint', async () => {
result = await contract.getMintedNfts()
expect(result).to.have.lengthOf(1)
await contract.connect(user1).mintNft({
value: toWei(0.005),
})
result = await contract.getMintedNfts()
expect(result).to.have.lengthOf(2)
})
})
describe('Breeding', () => {
beforeEach(async () => {
await contract.connect(user1).mintNft({
value: toWei(0.005),
})
})
it('it should confirm nft breeding', async () => {
result = await contract.getAllNfts()
expect(result).to.have.lengthOf(2)
result = await contract.getBreededNfts()
expect(result).to.have.lengthOf(0)
const father = await contract.getNft(fatherTokenId)
const mother = await contract.getNft(motherTokenId)
await contract.connect(user1).breedNft(father.id.toNumber(), mother.id.toNumber(), {
value: toWei(0.005),
})
result = await contract.getMyNfts()
expect(result).to.have.lengthOf(1)
result = await contract.getBreededNfts()
expect(result).to.have.lengthOf(1)
})
it('it should confirm nft breeded parents', async () => {
const father = await contract.getNft(fatherTokenId)
const mother = await contract.getNft(motherTokenId)
await contract.breedNft(father.id.toNumber(), mother.id.toNumber(), {
value: toWei(0.005),
})
const child = await contract.getNft(childTokenId)
expect(father.id).to.be.equal(child.traits.parents[0])
expect(mother.id).to.be.equal(child.traits.parents[1])
result = await contract.getParentsOf(child.id)
expect(result).to.have.lengthOf(2)
expect(result[0].id).to.be.equal(father.id)
expect(result[1].id).to.be.equal(mother.id)
expect(father.traits.weapon).to.be.equal(child.traits.weapon)
expect(mother.traits.environment).to.be.equal(child.traits.environment)
})
})
})

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

The Deployment Script

The DappBreed deployment script is responsible for deploying the DappBreed 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 script defines the deployment parameters, including the contract_name, name, symbol, baseURI, and maxSupply.
    • These parameters specify the name, symbol, base URI, and maximum supply of the NFTs to be deployed.
  4. Contract Deployment:

    • The script uses the ethers.getContractFactory() method to obtain the contract factory for the DappBreed 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 DappBreed deployment script allows for the easy deployment of the DappBreed 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");
async function main() {
const contract_name = "DappBreed";
const name = "Dapp Breeds";
const symbol = "DAB";
const baseURI =
"https://ipfs.io/ipfs/QmTWbe9wDns7aqZQNCuWh5PqybGbBF91kngC5Zf8qmCoyg/";
const maxSupply = 99;
const Contract = await ethers.getContractFactory(contract_name);
const contract = await Contract.deploy(name, symbol, baseURI, maxSupply);
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.

Navbar Component

Navbar Component

The **Navbar** and **MobileMenu** components represent the user interface elements of the DappBreeds application's navigation bar. They display the website's logo, menu items, and a connect wallet button.

The **Navbar** component adjusts its layout based on screen size, while the **MobileMenu** component is specifically designed for mobile devices. The **ConnectButton** component handles the logic for connecting the user's wallet. See code below:

import { React, useState } from 'react'
import { Link } from 'react-router-dom'
import { AiOutlineClose } from 'react-icons/ai'
import { BiMenuAltRight } from 'react-icons/bi'
import { navdata } from '../constants'
import { truncate, useGlobalState } from '../store'
import { connectWallet } from '../services/blockchain'
const MobileMenu = () => {
const [breeds] = useGlobalState('breeds')
return (
<div className="p-6 fixed top-20 right-0 mx-4 my-2 w-auto flex bg-[#1b1b1b] rounded-xl tilt-in-fwd-tr shadow-md z-10 ">
<ul className="flex flex-col space-y-8 text-white align-center justify-center items-start align-center">
{navdata.map((data) => (
<li
key={data.id}
className=" flex items-center cursor-pointer font-bold hover:text-blue-700"
>
<Link to={data.path}>
<div className="relative">
<span>{data.title}</span>
{data.counter && breeds.length > 0 && (
<span className="bg-white p-1 w-1 rounded-full text-black">
{breeds.length}
</span>
)}
</div>
</Link>
</li>
))}
<li className=" flex items-center shadow-md cursor-pointer hover:text-gray-500">
<ConnectButton />
</li>
</ul>
</div>
)
}
const Navbar = () => {
const [toggleMenu, setToggleMenu] = useState(false)
const [breeds] = useGlobalState('breeds')
return (
<div
className="flex fixed top-0 right-0 left-0 justify-between
items-center w-full h-[80px] bg-gray-400 px-2 lg:px-10 z-10"
>
<Link to="/" className="font-bold">
DappBreed
</Link>
<div className="hidden justify-between items-center gap-10 md:flex ">
<div className="flex gap-5">
{navdata.map((data) => (
<div key={data.id}>
<Link to={data.path}>
<div className="font-bold hover:text-blue-700 ">
<span>{data.title}</span>
{data.counter && breeds.length > 0 && (
<span className="bg-white p-1 w-1 rounded-full text-black">
{breeds.length}
</span>
)}
</div>
</Link>
</div>
))}
</div>
<div className="flex justify-start mr-20">
<ConnectButton />
</div>
</div>
<div className="flex md:hidden">
{toggleMenu ? (
<AiOutlineClose
className="text-3xl cursor-pointer"
onClick={() => setToggleMenu(!toggleMenu)}
/>
) : (
<BiMenuAltRight
className="cursor-pointer text-3xl"
onClick={() => setToggleMenu(!toggleMenu)}
/>
)}
<div className={`${!toggleMenu ? 'hidden' : 'flex'}`}>
<MobileMenu />
</div>
</div>
</div>
)
}
const ConnectButton = () => {
const [connectedAccount] = useGlobalState('connectedAccount')
return connectedAccount ? (
<button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-4
px-6 rounded-full transition-all duration-300 shadow-md shadow-black"
>
{truncate(connectedAccount, 4, 4, 11)}
</button>
) : (
<button
onClick={connectWallet}
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-4
px-6 rounded-full transition-all duration-300 shadow-md shadow-black"
>
Connect Wallet
</button>
)
}
export default Navbar
view raw Navbar.jsx hosted with ❤ by GitHub

Hero Component

Hero Component

The **Hero** component represents a section of the DappBreeds application's homepage. It consists of two sub-components, **HeroActions** and **HeroImage**, and it arranges them in a responsive layout.

The **HeroActions** component contains buttons for minting and breeding NFTs, along with associated logic for interacting with the blockchain.

The **HeroImage** component displays an image related to the hero section. The **Hero** component aims to engage users by showcasing the possibilities of creating and breeding NFTs, along with statistics related to artworks, artists, and breeds. See the code below:

import React from 'react'
import Heroimage3 from '../assets/heroimage3.jpg'
import { setGlobalState, useGlobalState } from '../store'
import { toast } from 'react-toastify'
import { breedNft, mintNft } from '../services/blockchain'
const Hero = () => {
return (
<div className="mt-20 flex flex-col-reverse gap-10 items-center justify-center lg:flex-row px-2 md:px-10 ">
<HeroActions />
<HeroImage />
</div>
)
}
const HeroActions = () => {
const [breeds] = useGlobalState('breeds')
const [connectedAccount] = useGlobalState('connectedAccount')
const onMint = async () => {
if (!connectedAccount) return toast.warning('Wallet not connected')
await toast.promise(
new Promise(async (resolve, reject) => {
await mintNft()
.then((tx) => {
console.log(tx)
resolve(tx)
})
.catch((error) => {
console.log(error)
reject(error)
})
}),
{
pending: 'Approve transaction...',
success: 'NFT Minted successfully 👌',
error: 'Encountered error 🤯',
}
)
}
const onBreed = async () => {
if (!connectedAccount) return toast.warning('Wallet not connected')
const fatherId = breeds[0].id
const motherId = breeds[1].id
await toast.promise(
new Promise(async (resolve, reject) => {
await breedNft(fatherId, motherId)
.then((tx) => {
console.log(tx)
setGlobalState('breeds', [])
resolve(tx)
})
.catch((error) => {
console.log(error)
reject(error)
})
}),
{
pending: 'Approve transaction...',
success: 'NFT Breeded successfully 👌',
error: 'Encountered error 🤯',
}
)
}
return (
<div className="flex flex-col gap-8 items-start justify-center ">
<div className=" flex flex-col font-bold text-white gap-5">
<div className=" text-3xl sm:text-5xl font-bold md:text-7xl ">
<h2>Create Your</h2>
<h2>
Own<span>NFT Dream </span>
</h2>
<h2>Gallery</h2>
</div>
<p className="flex w-auto md:w-[450px] font-normal">
Take it a step further and explore the exciting world of NFT breeding.
Combine traits, characteristics, and attributes of your existing NFTs
to create entirely new and extraordinary pieces. Unleash your
creativity and experiment with endless possibilities to breed your own
masterpiece.
</p>
</div>
<div className="flex gap-5">
<button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold
rounded-full transition-all duration-300 py-4 px-6 sm:px-14 shadow-md shadow-black"
onClick={onMint}
>
Mint
</button>
<button
className="border-2 border-blue-500 hover:bg-blue-500 hover:text-white text-blue-500 font-bold
rounded-full transition-all duration-300 py-4 px-6 sm:px-14 space-x-1 shadow-md shadow-black"
onClick={onBreed}
>
<span>Breed Now</span>
{breeds.length > 0 && (
<span className="bg-white p-1 rounded-full text-black">
{breeds.length}
</span>
)}
</button>
</div>
<div className="flex justify-center items-center ml-5 gap-10 text-white ">
<div>
<span>Artworks</span>
<p className="font-bold">21.5k </p>
</div>
<div>
<span>Artist</span>
<p className="font-bold">15.6k </p>
</div>
<div>
<span>Breeds</span>
<p className="font-bold">21.5k </p>
</div>
</div>
</div>
)
}
const HeroImage = () => (
<div className="flex flex-1 items-center justify-center">
<img
src={Heroimage3}
alt="HeroImage3"
className="h-[20rem] object-cover rounded-md border-2 border-blue-500 flex items-end justify-end md:h-[25rem]"
/>
</div>
)
export default Hero
view raw Hero.jsx hosted with ❤ by GitHub

Sponsors Component

Sponsors Component

The **Sponsors** component is responsible for displaying a list of sponsor logos on the DappBreeds website. It renders a list (**ul**) element with each sponsor's logo represented by an **img** element. The logos are sourced from local image files (**coinbase.png**, **dropbox.png**, **slack.png**, **webflow.png**) and displayed with a fixed width of 200 pixels while maintaining their aspect ratios.

The component utilizes flexbox and responsive design (**md:flex-row**) to align the logos horizontally on larger screens and vertically on smaller screens. The **Sponsors** component enhances the visual appeal of the website and showcases the support of notable sponsors.

Please ensure that you have the copies of all the images found in this link inside assets folder in the src directory of your project.

Use the code below to form the Sponsors component.

import React from 'react'
import coinbase from '../assets/coinbase.png'
import dropbox from '../assets/dropbox.png'
import slack from '../assets/slack.png'
import webflow from '../assets/webflow.png'
const Sponsors = () => {
return (
<ul className="flex flex-col md:flex-row justify-around items-center p-10 ">
<li>
<img
src={dropbox}
alt="dropbox"
className=" flex w-[200px] object-contain"
/>
</li>
<li>
<img
src={coinbase}
alt="coinbase"
className=" flex w-[200px] object-contain"
/>
</li>
<li>
<img
src={slack}
alt="slack"
className=" flex w-[200px] object-contain"
/>
</li>
<li>
<img
src={webflow}
alt="webflow"
className=" flex w-[200px] object-contain"
/>
</li>
</ul>
)
}
export default Sponsors
view raw Sponsors.jsx hosted with ❤ by GitHub

Trending & Trending Card Component

Trending Components

The **Trending** component displays a section on the DappBreeds website that showcases trending NFTs. It dynamically renders a heading based on the length of the **nfts** collection, showing either "Trending" or "No minted NFTs yet...". It also includes the **TrendingCards** component to display a grid of NFT cards based on the provided **nfts** collection.

The component manages the number of displayed cards and handles loading more cards when available. It allows users to add or remove NFTs from the lab and provides truncate functionality for displaying NFT information.

The **Trending** component is a central element for showcasing popular NFTs, providing interactivity, and enabling users to explore and engage with the NFT collection.

The **TrendingCards** component receives a **collection** prop and renders a grid of NFT cards based on the provided collection. It dynamically updates the displayed cards using the **useState** and **useEffect** hooks.

Each card contains an image, name, description, mint cost, owner, and an action button. The component allows users to add or remove NFTs from the lab and offers a "Load more" button to fetch additional cards from the collection.

With its grid layout and interactive features, **TrendingCards** enhances the visual representation of NFTs, promotes engagement, and enables users to explore and interact with the trending NFTs featured in the DappBreeds application.

Use the codes below for creating each components, ensure they are in different files.

view raw Trending.jsx hosted with ❤ by GitHub
import React, { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import { truncate } from '../utils/faker'
import { addToLab, remFromLab, useGlobalState } from '../store'
const TrendingCards = ({ collection }) => {
const [breeds] = useGlobalState('breeds')
const [end, setEnd] = useState(4)
const [count] = useState(4)
const [nfts, setNfts] = useState([])
const getNfts = () => {
return collection.slice(0, end)
}
useEffect(() => {
setNfts(getNfts())
}, [collection, end])
const onAddToLab = (nft) => {
if (breeds.some((breed) => breed.id == nft.id)) return
nft.selected = true
addToLab(nft)
}
const onRemFromLab = (nft) => {
if (!breeds.some((breed) => breed.id == nft.id)) return
nft.selected = false
remFromLab(nft)
}
return (
<div className="mt-20 w-full flex flex-col">
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-6 md:gap-4 lg:gap-3 py-2.5">
{nfts.map((nft, i) => (
<div
key={i}
className="max-w-xs sm:max-w-sm text-white overflow-hidden border border-blue-500 shadow-lg "
>
<div>
<Link to={'/nft/' + nft.id}>
<img
className="w-full block rounded-sm border-b border-blue-500 "
src={nft.traits.image}
alt="Sunset in the mountains"
/>
</Link>
</div>
<Link
to={'/nft/' + nft.id}
className="p-4 flex flex-col items-center gap-2"
>
<div className="flex flex-col gap-2 font-bold text-xl text-blue-500">
<span>
{nft.traits.name} {nft.traits.weapon} &{' '}
{nft.traits.environment}
</span>
</div>
<span>{nft.description}</span>
<div className="flex flex-col gap-2 md:flex-row justify-between">
<p className="flex flex-col text-center">
<span className="font-bold space-x-1">
<span>{nft.mintCost}</span>
<span>ETH</span>
</span>
<span className="text-blue-500">
{truncate(nft.owner, 4, 4, 11)}
</span>
</p>
</div>
</Link>
<div className="flex justify-center items-center py-4">
{nft.selected ? (
<button
onClick={() => onRemFromLab(nft)}
className="bg-blue-500 hover:bg-blue-600
text-white font-semibold hover:text-white
py-2 px-4 border-0 rounded-sm transition-all duration-300"
>
Remove
</button>
) : (
<button
onClick={() => onAddToLab(nft)}
className="bg-transparent hover:bg-blue-500
text-white font-semibold hover:text-white
py-2 px-4 border border-white hover:border-blue-500
rounded-sm transition-all duration-300"
>
Add Now
</button>
)}
</div>
</div>
))}
</div>
<div className="h-24" />
{collection.length > 0 && collection.length > nfts.length && (
<div className="flex justify-center items-center mx-auto">
<button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-4
px-6 rounded-full transition-all duration-300 shadow-lg shadow-black"
onClick={() => setEnd(end + count)}
>
Load more
</button>
</div>
)}
</div>
)
}
export default TrendingCards

Collection & Collection Card Component

Collections Components

The **Collection** component displays a collection of NFTs in a grid layout. It receives a **collection** prop containing the NFT data and an optional **title** prop for the collection title.

The component manages the number of displayed NFTs using the **useState** and **useEffect** hooks. It renders the **CollectionCard** component for each NFT in the **nfts** state. The component also provides a "Load more" button to fetch additional NFTs from the collection when available.

The **CollectionCard** component represents an individual NFT card within the **Collection** component. It receives an **nft** prop containing the NFT data and renders the card's image, name, description, and breeding information.

The component dynamically displays whether the NFT was bred or minted based on the **nft** data. It also includes a link to the detailed view of the NFT. **CollectionCard** enhances the visual representation of NFTs within the collection, providing concise information about each NFT and enabling users to navigate to specific NFT details for further exploration.

Use the codes below to create their individual files.

import React, { useEffect, useState } from 'react'
import CollectionCard from './CollectionCard'
const Collection = ({ collection, title }) => {
const [end, setEnd] = useState(4)
const [count] = useState(4)
const [nfts, setNfts] = useState([])
const getNfts = () => {
return collection.slice(0, end)
}
useEffect(() => {
setNfts(getNfts())
}, [collection, end])
return collection?.length > 0 ? (
<div className="mt-5 flex flex-col w-full mx-auto">
<div className="flex items-center justify-center ">
<h2 className="font-extrabold text-3xl md:text-5xl text-white ">
{title ? title : 'Recent Breedings'}
</h2>
</div>
<div className="p-10 pt-0 md:p-20 w-full">
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-6 md:gap-4 lg:gap-3 py-2.5">
{nfts.map((nft, i) => (
<CollectionCard key={i} nft={nft} />
))}
</div>
</div>
{collection.length > 0 && collection.length > nfts.length && (
<div className="flex justify-center items-center mx-auto">
<button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-4
px-6 rounded-full transition-all duration-300 shadow-lg shadow-black"
onClick={() => setEnd(end + count)}
>
Load more
</button>
</div>
)}
</div>
) : null
}
export default Collection
view raw Collection.jsx hosted with ❤ by GitHub
import React from 'react'
import { Link } from 'react-router-dom'
const CollectionCard = ({ nft }) => (
<Link
to={'/nft/' + nft.id}
className="max-w-xs sm:max-w-sm text-white overflow-hidden
border-2 rounded-xl border-gray-400 shadow-lg"
>
<div className="flex justify-start items-center gap-2 p-4">
<img
src={nft.traits.image}
alt={nft.traits.name}
className=" w-12 h-24 object-cover"
/>
<div className="flex-col gap-2">
<h2 className=" font-extrabold">{nft.traits.name}</h2>
<p>{nft.traits.description}</p>
{nft.traits.breeded ? (
<span className="text-sm font-semibold text-blue-500">
{nft.traits.weapon} & {nft.traits.environment} (Inherited)
</span>
) : (
<span className="text-sm font-semibold text-blue-500">
{nft.traits.weapon} & {nft.traits.environment} (Minted)
</span>
)}
</div>
</div>
</Link>
)
export default CollectionCard

Footer Component

The Footer Component

The **Footer** component represents the footer section of the application. It is a static component that displays relevant information and links. See the code below.

view raw Footer.jsx hosted with ❤ by GitHub

Spacer Component

Spacer Component

The **Spacer** component is a simple utility component that creates vertical spacing in a layout by rendering empty **div** elements. See the codes below.

import React from 'react'
const Spacer = ({ spaces }) => {
const range = Array.from({ length: spaces }, (_, index) => index)
return (
<div>
{range.map((_, i) => (
<div key={i} className="h-10" />
))}
</div>
)
}
export default Spacer
view raw Spacer.jsx hosted with ❤ by GitHub

Chat Button Component

Not Authed
When Authed

The **ChatButton** component is a menu component that provides chat-related functionalities. It renders a button with an "Add" icon and displays a dropdown menu when clicked.

The dropdown menu options change based on the user's authentication status. If the user is not logged in, the menu displays options for signing up and logging in. If the user is logged in, the menu shows options for recent chats and logging out.

Each menu item triggers a specific action, such as signing up, logging in, opening the list of recent chats, or logging out. The actions are asynchronous and utilize the **toast.promise** function to display informative toast messages during the operations.

The component utilizes CometChat SDK and Tailwind CSS to offers a user-friendly way to interact with chat-related functionality in a concise and intuitive manner. See the code below.

import React from 'react'
import { AiOutlinePlus } from 'react-icons/ai'
import { FiLogIn } from 'react-icons/fi'
import { HiLogin } from 'react-icons/hi'
import { LiaUserFriendsSolid } from 'react-icons/lia'
import { SiGnuprivacyguard } from 'react-icons/si'
import { Menu } from '@headlessui/react'
import { toast } from 'react-toastify'
import {
logOutWithCometChat,
loginWithCometChat,
signUpWithCometChat,
} from '../services/chat'
import { setGlobalState, useGlobalState } from '../store'
const ChatButton = () => {
const [connectedAccount] = useGlobalState('connectedAccount')
const [currentUser] = useGlobalState('currentUser')
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 🤯',
}
)
}
return (
<Menu
as="div"
className="inline-block text-left mx-auto fixed right-5 bottom-[80px]"
>
<Menu.Button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold
rounded-full transition-all duration-300 p-3 focus:outline-none
focus-visible:ring-2 focus-visible:ring-white
focus-visible:ring-opacity-75 shadow-md shadow-black"
as="button"
>
<AiOutlinePlus size={20} />
</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-red-500'
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
onClick={handleSignUp}
>
<SiGnuprivacyguard size={17} />
<span>Chat SignUp</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>
</>
) : (
<>
<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('chatListModal', 'scale-100')}
>
<LiaUserFriendsSolid size={17} />
<span>Recent 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

Chat List Component

The Chat List Component

The **ChatList** component displays a modal dialog that contains a list of conversations. It is triggered by the **chatListModal** state variable.

The modal dialog appears as an overlay on top of the page content, providing a darkened background to focus on the conversation list. The component retrieves the user's conversations using the **getConversations** function and displays them in the list.

Each conversation is rendered as a **Conversation** component, showing the conversation participant's avatar, truncated username, and last message.

Clicking on a conversation triggers navigation to the chat room associated with that conversation, updates the global state accordingly, and retrieves the chat messages using the **getMessages** function.

The component utilizes **react-identicons** to generate unique avatars based on the conversation participant's username. The chat list modal can be closed by clicking the "X" button. See the codes for it below.

import React, { useEffect, useState } from 'react'
import { FaTimes } from 'react-icons/fa'
import Identicon from 'react-identicons'
import { setGlobalState, truncate, useGlobalState } from '../store'
import { getConversations, getMessages } from '../services/chat'
import { useNavigate } from 'react-router-dom'
const ChatList = () => {
const [chatListModal] = useGlobalState('chatListModal')
const [currentUser] = useGlobalState('currentUser')
const [conversations] = useGlobalState('conversations')
useEffect(() => {
getConversations().then((convs) => setGlobalState('conversations', convs))
}, [currentUser])
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 ${chatListModal}`}
>
<div className="bg-[#222121] text-gray-400 shadow-lg shadow-slate-900 rounded-xl w-11/12 md:w-2/5 h-7/12 py-6">
<div className="flex flex-col">
<div className="flex justify-between items-center px-6">
<p className="font-semibold">Chat List</p>
<button
type="button"
className="border-0 bg-transparent focus:outline-none"
onClick={() => setGlobalState('chatListModal', 'scale-0')}
>
<FaTimes className="text-gray-400" />
</button>
</div>
<div className="flex flex-col justify-center items-start mt-5 mb-5 max-h-60 overflow-y-auto">
{conversations.map((conv, i) => (
<Conversation
key={i}
text={conv.lastMessage.text}
owner={conv.conversationWith.uid}
time={Number(conv.lastMessage.sentAt + '000')}
/>
))}
</div>
</div>
</div>
</div>
)
}
const Conversation = ({ text, owner, time }) => {
const navigate = useNavigate()
const navTo = () => {
navigate('/chats/' + owner)
setGlobalState('chatListModal', 'scale-0')
getMessages(owner).then((msgs) => setGlobalState('messages', msgs))
}
return (
<div
onClick={navTo}
className="flex justify-between items-center mb-4 w-full hover:bg-[#151414]
transition-all duration-300 px-6 py-2 cursor-pointer"
>
<div className="flex items-center">
<Identicon
className="w-12 h-12 rounded-full object-cover mr-4 shadow-md bg-gray-400"
string={owner}
size={35}
/>
<div>
<h3 className="text-lg font-bold">{truncate(owner, 4, 4, 11)}</h3>
<p className="text-gray-500">{truncate(text, 37, 0, 40)}</p>
</div>
</div>
<p className="text-sm text-gray-500 ml-auto">
{new Date(time).toLocaleString()}
</p>
</div>
)
}
export default ChatList
view raw ChatList.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.

Home Page

The Home Page Component

The **Home** page component represents the home page of the application. It includes several components to provide the main features and content of the page.

The components used in the **Home** page are:

  • **Hero**: This component displays a hero section with a catchy title and description, inviting users to explore the features of the application.

  • **Sponsors**: This component showcases the logos of sponsors or partner companies, adding credibility and promoting collaboration.

  • **Trending**: This component displays a section for trending items or NFTs. It receives the **minted** array as a prop, which represents the collection of recently minted NFTs.

  • **Collection**: This component represents a collection section, showing a grid of NFTs. It is conditionally rendered based on the length of the **breeded** array, which contains NFTs that have been created through breeding.

By combining these components, the **Home** page provides a comprehensive overview of the application, including a hero section, sponsor logos, trending NFTs, and a collection of bred NFTs if any exist. See the codes below.

import React from 'react'
import Collection from '../components/Collection'
import Hero from '../components/Hero'
import Sponsors from '../components/Sponsors'
import Trending from '../components/Trending'
import { useGlobalState } from '../store'
const Home = () => {
const [minted] = useGlobalState('minted')
const [breeded] = useGlobalState('breeded')
return (
<div>
<Hero />
<Sponsors />
<Trending nfts={minted} />
{breeded.length > 0 && <Collection collection={breeded} />}
</div>
)
}
export default Home
view raw Home.jsx hosted with ❤ by GitHub

Details Page

The Single NFT Page

The **Details** page component represents a page that displays the details of a specific NFT. It fetches the NFT data using the **getAnNft** function and renders the information once the data is loaded.

The components used in the **Details** page are:

  • **NFTImage**: This component displays the image of the NFT. It receives the **nft** object as a prop and renders the image using the **traits.image** property.

  • **NFTInfo**: This component displays the detailed information of the NFT, including the NFT's name, owner, description, mint cost, weapon, environment, and a button to initiate a chat with the owner. It receives the **nft** object as a prop and renders the information accordingly.

  • **CollectionCard**: This component is used to display NFT cards. It is utilized within the **Details** page to showcase the inherited NFTs from parents if they exist.

The **Details** page also conditionally renders the inherited NFTs section if the **parents** array has a length greater than zero. It displays the cards of the inherited NFTs using the **CollectionCard** component.

Overall, the **Details** page provides a comprehensive view of a specific NFT, including its image, detailed information, and any inherited NFTs. See the codes below.

import { useEffect, useState } from 'react'
import { getAnNft } from '../services/blockchain'
import { useParams } from 'react-router-dom'
import { truncate, useGlobalState } from '../store'
import { useNavigate } from 'react-router-dom'
import CollectionCard from '../components/CollectionCard'
const Details = () => {
const [nft] = useGlobalState('nft')
const [parents] = useGlobalState('parents')
const { id } = useParams()
const [loaded, setLoaded] = useState(false)
useEffect(() => {
const fetchData = async () => {
await getAnNft(id)
setLoaded(true)
}
fetchData()
}, [id])
return loaded ? (
<div className="w-full flex flex-col">
<div className="flex flex-col p-5 w-full items-center justify-center lg:flex-row gap-20 mt-10">
<NFTImage nft={nft} />
<NFTInfo nft={nft} />
</div>
{parents.length > 0 && (
<div className="mb-10 p-10 md:p-20 w-full space-y-10">
<h2 className="font-extrabold text-3xl md:text-5xl text-white text-center">
Inherited From
</h2>
<div className="flex justify-center items-center space-x-4">
{parents.map((nft, i) => (
<CollectionCard key={i} nft={nft} />
))}
</div>
</div>
)}
</div>
) : (
<div className="w-full flex flex-col">
<h2 className="font-extrabold text-3xl md:text-5xl text-white text-center">
Loading...
</h2>
</div>
)
}
const NFTInfo = ({ nft }) => {
const navigate = useNavigate()
return (
<div className="flex flex-col items-start gap-5 w-full md:w-2/6 text-white">
<div className="flex flex-col gap-3">
<h4 className="text-white">{nft.traits.name}</h4>
<span>@{truncate(nft.owner, 4, 4, 11)}</span>
<p>{nft.traits.description}</p>
</div>
<div>
<h4>Mint Cost</h4>
<span>{nft.mintCost} ETH</span>
</div>
<div className="flex space-x-4">
<div>
<h6 className="mb-4 font-semibold md:justify-start text-blue-500">
Weapon
</h6>
<ul className="flex space-x-3">
<li className="text-neutral-600 dark:text-neutral-200">
{nft.traits.weapon}
</li>
</ul>
</div>
<div>
<h6 className="mb-4 font-semibold md:justify-start text-blue-500">
Environment
</h6>
<ul className="flex space-x-3">
<li className="text-neutral-600 dark:text-neutral-200">
{nft.traits.environment}
</li>
</ul>
</div>
</div>
<button
onClick={() => navigate('/chats/' + nft.owner)}
className="bg-transparent hover:bg-blue-500
text-white font-semibold hover:text-white
py-2 px-4 border border-white hover:border-blue-500
rounded-sm transition-all duration-300"
>
Chat with Owner
</button>
</div>
)
}
const NFTImage = ({ nft }) => (
<div className="flex">
<li className="bg-[#202938] w-full md:w-[340px] h-[340px] flex justify-center items-center">
<img
src={nft.traits.image}
alt="DetailsImage"
className="w-full h-72 object-contain p-4 md:p-0"
/>
</li>
</div>
)
export default Details
view raw Details.jsx hosted with ❤ by GitHub

Collections Page

Your Personal Collections Page

The Collections component represents a page that displays the user's collection of NFTs. It fetches the user's NFT collection using the getMyNfts function and renders the collection if it contains any NFTs.

The components used in the Collections page are:

  • Collection: This component displays a collection of NFT cards. It receives the collection array as a prop and renders the collection using the CollectionCard component.

The Collections page fetches the user's NFT collection when the component mounts using the useEffect hook.

If the collection array has a length greater than zero, the page renders the Collection component with the user's collection and provides a title of "Your Collection". If the collection array is empty, it displays a message indicating that the user has no collection yet.

Overall, the Collections page provides a user-specific view of their NFT collection, showcasing the NFT cards using the Collection component if the collection is not empty, or displaying a message if the collection is empty. See the code below.

import React, { useEffect } from 'react'
import { useGlobalState } from '../store'
import Collection from '../components/Collection'
import { getMyNfts } from '../services/blockchain'
const Collections = () => {
const [collection] = useGlobalState('collection')
useEffect(() => {
const fetchData = async () => {
await getMyNfts()
}
fetchData()
}, [])
return collection.length > 0 ? (
<div className="mt-4 p-10 flex flex-col w-full ">
<Collection collection={collection} title="Your Collection" />
</div>
) : (
<div className="mt-4 p-10 flex flex-col w-full ">
<h2 className="font-extrabold text-3xl md:text-5xl text-white text-center">
You have no collection yet...
</h2>
</div>
)
}
export default Collections
view raw Collections.jsx hosted with ❤ by GitHub

The Lab Page

The Labs Page

The **Lab** component represents a page where users can interact with the breeding feature of the application. Users can select NFTs from their collection, breed them, and perform related actions.

The **Lab** page allows users to breed NFTs by selecting two NFTs from their collection as father and mother. It features three important buttons:

  1. "Randomly Select": This button randomly selects two NFTs from the platform’s collection as father and mother, facilitating a quick breeding process.

  2. "Breed Now": This button initiates the breeding process with the selected father and mother NFTs. It triggers the **breedNft** function, which performs the necessary blockchain transactions for breeding. Success and error messages are displayed using the **toast** component.

  3. "Clear Selection": This button clears the current selection of father and mother NFTs, allowing users to choose different NFTs for breeding.

The page also displays visual representations of the selected NFTs and checks the user's wallet connection before breeding. If the wallet is not connected, a warning message is displayed using the **toast** component. See the codes below.

import React from 'react'
import { AiOutlinePlus } from 'react-icons/ai'
import { setGlobalState, useGlobalState } from '../store'
import { breedNft } from '../services/blockchain'
import { toast } from 'react-toastify'
const Lab = () => {
const [nfts] = useGlobalState('nfts')
const [breeds] = useGlobalState('breeds')
const [connectedAccount] = useGlobalState('connectedAccount')
const onBreed = async () => {
if (!connectedAccount) return toast.warning('Wallet not connected')
const fatherId = breeds[0].id
const motherId = breeds[1].id
await toast.promise(
new Promise(async (resolve, reject) => {
await breedNft(fatherId, motherId)
.then((tx) => {
console.log(tx)
setGlobalState('breeds', [])
resolve(tx)
})
.catch((error) => {
console.log(error)
reject(error)
})
}),
{
pending: 'Approve transaction...',
success: 'NFT Breeded successfully 👌',
error: 'Encountered error 🤯',
}
)
}
const onRandomSelect = () => {
setGlobalState('breeds', shuffleArray(nfts).slice(0, 2))
}
const shuffleArray = (array) => {
const shuffledArray = [...array]
for (let i = shuffledArray.length - 1; i > 0; i--) {
const randomIndex = Math.floor(Math.random() * (i + 1))
;[shuffledArray[i], shuffledArray[randomIndex]] = [
shuffledArray[randomIndex],
shuffledArray[i],
]
}
return shuffledArray
}
return (
<div className="mt-4 mb-10">
{breeds.length > 0 ? (
<div className=" p-10 md:p-20 flex flex-col gap-20 items-center justify-center lg:flex-row px-2">
{breeds.map((nft, i) => (
<div
key={i}
className="flex items-center justify-center cursor-pointer relative
h-[20rem] w-[20rem] bg-transparent border-2 border-blue-500"
style={{ backgroundImage: `url(${nft.traits.image})` }}
>
<div className="absolute inset-0 bg-black opacity-50">
<div
className="flex flex-col justify-center items-center
gap-3 text-white h-full font-bold text-5xl"
>
<span>{nft.traits.name}</span>
</div>
</div>
</div>
))}
</div>
) : (
<div className="p-10 md:p-20 flex flex-col gap-20 items-center justify-center lg:flex-row px-2">
<div
className="flex items-center justify-center cursor-pointer
h-[20rem] w-[20rem] bg-transparent border-2 border-blue-500"
>
<div className="flex flex-col items-center gap-3 ">
<AiOutlinePlus size={32} />
<span>Add Father</span>
</div>
</div>
<div
className="flex items-center justify-center cursor-pointer
h-[20rem] w-[20rem] bg-transparent border-2 border-blue-500"
>
<div className="flex flex-col items-center gap-3 ">
<AiOutlinePlus size={32} />
<span>Add Mother</span>
</div>
</div>
</div>
)}
<div className="py-4 flex flex-col md:flex-row justify-center items-center gap-4">
<button
className="bg-transparent hover:bg-blue-500
text-white font-semibold hover:text-white
py-2 px-4 border border-white hover:border-blue-500
rounded-sm transition-all duration-300"
onClick={onRandomSelect}
>
Randomly Select
</button>
{breeds.length >= 2 && (
<button
className="bg-transparent hover:bg-blue-500
text-white font-semibold hover:text-white
py-2 px-4 border border-white hover:border-blue-500
rounded-sm transition-all duration-300"
onClick={onBreed}
>
Breed Now
</button>
)}
<button
className="bg-transparent hover:bg-blue-500
text-white font-semibold hover:text-white
py-2 px-4 border border-white hover:border-blue-500
rounded-sm transition-all duration-300"
onClick={() => setGlobalState('breeds', [])}
>
Clear Selection
</button>
</div>
</div>
)
}
export default Lab
view raw Lab.jsx hosted with ❤ by GitHub

The Chat Page

The Chat Page

The Chat page allows users to engage in a chat conversation with another user. It features a chat interface with the following components:

  1. Chat Messages: Messages exchanged between the users are displayed in a scrollable container. Each message is represented by the Message component, which shows the sender's avatar, username (truncated if necessary), message content, and the timestamp of the message.

  2. Message Input: Users can enter their messages in the input field at the bottom of the chat interface. When they submit the message, it is sent using the sendMessage function and displayed in the chat conversation. The input field supports placeholder text and automatically adjusts its height as the user types.

  3. Scroll to End: The chat interface automatically scrolls to the latest message, ensuring that the most recent content is always visible to the users.

The Chat page initializes the chat conversation by fetching existing messages using the getMessages function. It also listens for real-time updates using the listenForMessage function, allowing for a dynamic chat experience. Messages are stored in the global state using the setGlobalState function. See the codes below.

import React, { useEffect, useState } from 'react'
import Identicon from 'react-identicons'
import { getMessages, listenForMessage, sendMessage } from '../services/chat'
import { useParams } from 'react-router-dom'
import { setGlobalState, truncate, useGlobalState } from '../store'
const Chat = () => {
const [message, setMessage] = useState('')
const [messages] = useGlobalState('messages')
const [connectedAccount] = useGlobalState('connectedAccount')
const { id } = useParams()
const onSendMessage = async (e) => {
e.preventDefault()
if (!message) return
await sendMessage(id, message).then((msg) => {
setGlobalState('messages', (prevState) => [...prevState, msg])
setMessage('')
scrollToEnd()
})
}
useEffect(() => {
const fetchData = async () => {
await getMessages(id).then((msgs) => {
setGlobalState('messages', msgs)
scrollToEnd()
})
await listenForMessage(id).then((msg) => {
setGlobalState('messages', (prevState) => [...prevState, msg])
scrollToEnd()
})
}
fetchData()
}, [])
const scrollToEnd = () => {
const elmnt = document.getElementById('messages-container')
elmnt.scrollTop = elmnt.scrollHeight
}
return (
<div className="mt-4 mb-10 pt-10 md:p-20 flex flex-col justify-center items-center w-full">
<div className="bg-[#222121] text-gray-400 sm:shadow-lg shadow-slate-900 rounded-xl w-full md:w-2/5 h-7/12 py-6">
<div className="flex flex-col">
<div className="flex justify-between items-center px-6">
<p className="font-semibold">Chat</p>
</div>
<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.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 className="h-[4rem] w-full mt-4 px-6" onSubmit={onSendMessage}>
<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>
)
}
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

Great work! We have now completed all of the components and pages for this project. Let's now connect them to their respective services. We will start with the App component.

App Entry Component

The App component is the entry point of the application and sets up the routing and main structure of the app. It includes the following components and functionality:

  1. Navigation: The Navbar component is displayed at the top of the app, providing navigation links to different pages.

  2. Routing: The Routes component from react-router-dom is used to define the routes and their corresponding components. Each route is associated with a specific URL path and renders the appropriate component when the path is matched.

  3. Pages: The Home, Details, Collections, Lab, and Chat components are defined as routes and rendered based on the current URL path. These components represent the different pages of the application, such as the home page, details page, collections page, lab page, and chat page.

  4. Data Loading: The useEffect hook is used to trigger data loading functions when the component mounts. It checks if a wallet is connected using isWalletConnected, loads data using loadData, and checks the authentication state using checkAuthState. This ensures that necessary data and authentication are available before rendering the app.

  5. Chat Functionality: The ChatButton component is conditionally rendered if there is a connected account. It provides a button to open the chat interface. The ChatList component is also rendered, displaying the list of chat conversations.

  6. Styling and Toasts: The Spacer component is used to create vertical spacing within the app layout. The Footer component is displayed at the bottom of the app. The ToastContainer component from react-toastify is used to display toast notifications for user feedback.

Replace the existing App.jsx component with the code below.

import { Routes, Route } from 'react-router-dom'
import Navbar from './components/Navbar'
import Footer from './components/Footer'
import Home from './pages/Home'
import Details from './pages/Details'
import Collections from './pages/Collections'
import Lab from './pages/Lab'
import { isWalletConnected, loadData } from './services/blockchain'
import { useEffect } from 'react'
import { ToastContainer } from 'react-toastify'
import ChatButton from './components/ChatButton'
import { checkAuthState } from './services/chat'
import ChatList from './components/ChatList'
import Spacer from './components/Spacer'
import Chat from './pages/Chat'
import { useGlobalState } from './store'
const App = () => {
const [connectedAccount] = useGlobalState('connectedAccount')
useEffect(() => {
isWalletConnected()
const fetchData = async () => {
await loadData()
await checkAuthState()
}
fetchData()
}, [])
return (
<div className="min-h-screen relative">
<Navbar />
<Spacer spaces={2} />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/nft/:id" element={<Details />} />
<Route path="/collections" element={<Collections />} />
<Route path="/lab" element={<Lab />} />
<Route path="/chats/:id" element={<Chat />} />
</Routes>
<Spacer spaces={7} />
<Footer />
{connectedAccount && <ChatButton />}
<ChatList />
<ToastContainer
position="bottom-center"
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
theme="dark"
/>
</div>
)
}
export default App
view raw App.jsx hosted with ❤ by GitHub

The Blockchain Service

The blockchain service script provides various functions and utilities to interact with the blockchain and manage NFT-related operations. Here's an overview of the key functionalities:

  1. Wallet Connection: isWalletConnected checks if the user's wallet (e.g., MetaMask) is connected and manages changes in the connected account. connectWallet triggers the wallet connection process.

  2. NFT Operations: mintNft and breedNft are used to mint and breed NFTs, respectively. They interact with the blockchain contract, handle transactions, and update the corresponding NFT collections.

  3. NFT Data Retrieval: Functions such as getAllNfts, getMintedNfts, getBreededNfts, getMyNfts, getAnNft, and getParentsNft fetch different types of NFT data from the blockchain contract. The retrieved data is structured and stored in the global state.

  4. Data Loading: loadData loads various NFT collections and related data, such as mint costs, when the application initializes.

  5. Utility Functions: toWei and fromWei convert between Wei and Ether units. structuredMint transforms raw contract data into a structured format for easier consumption and sorting.

  6. Error Handling: reportError logs any errors encountered during blockchain interactions.

Overall, the script facilitates wallet connection, NFT minting and breeding, data retrieval, and error handling for the blockchain-based NFT application.

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

import { getGlobalState, setGlobalState } from '../store'
import abi from '../abis/src/contracts/DappBreed.sol/DappBreed.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 = new ethers.providers.Web3Provider(ethereum)
const signer = provider.getSigner(accounts[0])
const contract = new ethers.Contract(ContractAddress, ContractAbi, signer)
return contract
}
const getContract = async () => {
const provider = new ethers.providers.JsonRpcProvider(
process.env.REACT_APP_RPC_URL
)
const wallet = ethers.Wallet.createRandom()
const signer = provider.getSigner(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 {
reportError('Please connect wallet.')
console.log('No accounts found.')
}
window.ethereum.on('chainChanged', (chainId) => {
window.location.reload()
})
window.ethereum.on('accountsChanged', async () => {
setGlobalState('connectedAccount', accounts[0])
await getMyNfts()
await logOutWithCometChat()
await isWalletConnected()
})
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 mintNft = async () => {
if (!ethereum) return alert('Please install metamask')
return new Promise(async (resolve, reject) => {
try {
const contract = await getEthereumContract()
const mintCost = getGlobalState('mintCost')
tx = await contract.mintNft({
value: toWei(mintCost),
})
await tx.wait()
await getMintedNfts()
resolve(tx)
} catch (err) {
reportError(err)
reject(err)
}
})
}
const breedNft = async (fatherId, motherId) => {
if (!ethereum) return alert('please install metamask')
return new Promise(async (resolve, reject) => {
try {
const contract = await getEthereumContract()
const mintCost = getGlobalState('mintCost')
tx = await contract.breedNft(fatherId, motherId, {
value: toWei(mintCost),
})
await tx.wait()
await getBreededNfts()
await getMintedNfts()
resolve(tx)
} catch (err) {
reportError(err)
reject(err)
}
})
}
const getAllNfts = async () => {
try {
const contract = await getContract()
const nfts = await contract.getAllNfts()
setGlobalState('nfts', structuredMint(nfts))
} catch (err) {
reportError(err)
}
}
const getMintedNfts = async () => {
try {
const contract = await getContract()
const nfts = await contract.getMintedNfts()
setGlobalState('minted', structuredMint(nfts))
} catch (err) {
reportError(err)
}
}
const getBreededNfts = async () => {
try {
const contract = await getContract()
const nfts = await contract.getBreededNfts()
setGlobalState('breeded', structuredMint(nfts))
} catch (err) {
reportError(err)
}
}
const getMyNfts = async () => {
try {
if (!ethereum) return console.log('please install metamask')
const contract = await getEthereumContract()
const nfts = await contract.getMyNfts()
setGlobalState('collection', structuredMint(nfts))
} catch (err) {
reportError(err)
}
}
const getAnNft = async (tokenId) => {
try {
const contract = await getContract()
const nft = await contract.getNft(tokenId)
await getParentsNft(tokenId)
setGlobalState('nft', structuredMint([nft])[0])
} catch (err) {
reportError(err)
}
}
const getParentsNft = async (tokenId) => {
try {
const contract = await getContract()
const nfts = await contract.getParentsOf(tokenId)
setGlobalState('parents', structuredMint(nfts))
} catch (err) {
reportError(err)
}
}
const loadData = async () => {
try {
const contract = await getContract()
const mintCost = await contract.mintCost()
await getMyNfts()
await getAllNfts()
await getMintedNfts()
await getBreededNfts()
setGlobalState('mintCost', fromWei(mintCost))
} catch (err) {
reportError(err)
}
}
const reportError = (error) => {
console.log(error)
}
const structuredMint = (mintData) =>
mintData
.map((mint) => ({
id: mint.id.toNumber(),
owner: mint.owner,
mintCost: fromWei(mint.mintCost),
timestamp: mint.timestamp.toNumber(),
traits: {
name: mint.traits.name,
description: mint.traits.description,
weapon: mint.traits.weapon,
image: mint.traits.image,
environment: mint.traits.environment,
rarity: mint.traits.rarity.toNumber(),
breeded: mint.traits.breeded,
},
}))
.sort((a, b) => b.timestamp - a.timestamp)
export {
connectWallet,
isWalletConnected,
mintNft,
breedNft,
getAllNfts,
getMintedNfts,
getBreededNfts,
getMyNfts,
getAnNft,
getParentsNft,
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: initCometChat initializes the CometChat SDK with the provided app ID and region.

  2. User Authentication: loginWithCometChat and signUpWithCometChat handle user authentication with CometChat using the provided user ID (UID) and authentication key. They log in or sign up the user respectively and return a Promise with the user object.

  3. User Logout: logOutWithCometChat logs out the currently logged-in user from CometChat and clears the current user state in the global state.

  4. User Authentication State: checkAuthState checks the authentication state of the user by fetching the currently logged-in user from CometChat and updates the current user state in the global state.

  5. Messaging: getMessages fetches the previous messages for a specific user, sendMessage sends a text message to a specified receiver ID, and listenForMessage listens for incoming text messages.

  6. Conversations: getConversations retrieves the conversations for the current user.

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 (UID) => {
const limit = 30
const messagesRequest = new CometChat.MessagesRequestBuilder()
.setUID(UID)
.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.USER
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),
})
)
})
}
export {
initCometChat,
loginWithCometChat,
signUpWithCometChat,
logOutWithCometChat,
getMessages,
sendMessage,
checkAuthState,
listenForMessage,
getConversations,
}
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.

The available global state variables include connectedAccount, chatListModal, conversations, messages, nfts, minted, breeded, collection, breeds, parents, nft, currentUser, and mintCost.

The service also includes a truncate function, which truncates text to a specified length, allowing for the display of shortened text while maintaining readability.
Additionally, there are utility functions such as addToLab and remFromLab that modify the breeds global state variable, allowing the addition and removal of items in the "Lab" section of the application.

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: '',
chatListModal: 'scale-0',
conversations: [],
messages: [],
nfts: [],
minted: [],
breeded: [],
collection: [],
breeds: [],
parents: [],
nft: null,
currentUser: null,
mintCost: 0,
})
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 addToLab = (nft) => {
const breeds = getGlobalState('breeds')
if (breeds.length == 2) return
setGlobalState('breeds', [nft, ...breeds])
}
const remFromLab = (nft) => {
const breeds = getGlobalState('breeds')
const index = breeds.findIndex((breed) => breed.tokenId == nft.tokenId)
breeds.splice(index, 1)
setGlobalState('breeds', [...breeds])
}
export {
setGlobalState,
useGlobalState,
getGlobalState,
addToLab,
remFromLab,
truncate,
}
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;
background-color: #222121;
}
@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 './index.css'
import 'react-toastify/dist/ReactToastify.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 building a Web3 NFT Cross-Breeding Dapp with real-time chat functionality using the CometChat SDK! If you are looking for a powerful and versatile chat SDK that can be used to add chat functionality to any application, I highly recommend trying out CometChat. CometChat offers a wide range of chat features, including 1-on-1 chat, group chat, file sharing, and more. It is also very easy to integrate with other platforms, making it a great choice for developers of all skill levels.

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

How to Build a web3 NFT Cross-Breeding Dapp with React, Solidity, and CometChat" is a transformative guide showcasing the fusion of blockchain, React, and real-time chat in creating an innovative NFT cross-breeding Dapp. The project leverages smart contracts for secure operations and offers an intuitive front-end interface. By incorporating CometChat, it enhances user engagement through real-time communication.

This tutorial demonstrates the potential of web3 development in revolutionizing the NFT space, providing transparency and secure transactions. Through comprehensive testing, the smart contracts ensure reliability. Developers are encouraged to explore the vast possibilities of blockchain technology.

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 (3)

Collapse
 
parth51199 profile image
parth51199

What are the benefits of using React, Solidity, and CometChat to build a web3 NFT cross-breeding DApp?

Collapse
 
daltonic profile image
Gospel Darlington

The answer is in the article!

Collapse
 
daltonic profile image
Gospel Darlington

How did the tutorial went?

Image of Docusign

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

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

Learn more