Eis o que você vai construir.
Introdução
Bem-vindo a este tutorial sobre a construção de um site de leilão de NFT usando React, Solidity e CometChat. Neste guia, nós o acompanharemos nas etapas de criação de um mercado descentralizado para a compra e venda de tokens não-fungíveis. Usaremos o React para o front end, o Solidity para o desenvolvimento de contratos inteligentes e o CometChat para mensagens e notificações em tempo real. Ao final deste tutorial, você terá uma plataforma de leilão de NFT totalmente funcional, pronta para entrar em funcionamento na blockchain Ethereum.
Pré-requisitos
Para seguir este tutorial, você precisará ter os itens abaixo instalados em sua máquina local. O NodeJs não é negociável, o restante pode ser instalado seguindo este guia, portanto, certifique-se de tê-lo instalado e funcionando.
- NodeJs
- React
- Solidity
- Tailwind
- CometChat SDK
- Hardhat
- EthersJs
- Metamask
- Yarn
Instalando as Dependências
Clone o starter kit (kit de iniciação rápida) usando o comando abaixo para sua pasta de projetos:
git clone https://github.com/Daltonic/tailwind_ethers_starter_kit
<PROJECT_NAME>
cd <PROJECT_NAME>
Em seguida, abra o projeto no VS Code ou em seu editor de código preferido. Localize o arquivo package.json e atualize-o com os códigos abaixo.
{
"name": "Auction",
"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.10",
"@nomiclabs/hardhat-ethers": "^2.1.0",
"@nomiclabs/hardhat-waffle": "^2.0.3",
"axios": "^1.2.1",
"ethereum-waffle": "^3.4.4",
"ethers": "^5.6.9",
"hardhat": "^2.10.1",
"ipfs-http-client": "55.0.1-rc.2",
"moment": "^2.29.4",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-hooks-global-state": "^1.0.2",
"react-icons": "^4.7.1",
"react-identicons": "^1.2.5",
"react-moment": "^1.1.2",
"react-router-dom": "6",
"react-scripts": "5.0.0",
"react-toastify": "^9.1.1",
"web-vitals": "^2.1.4"
},
"devDependencies": {
"@faker-js/faker": "^7.6.0",
"@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",
"express": "^4.18.2",
"express-fileupload": "^1.4.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",
"sharp": "^0.31.2",
"stream-browserify": "^3.0.0",
"stream-http": "^3.2.0",
"tailwindcss": "3.0.18",
"url": "^0.11.0",
"uuid": "^9.0.0"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
Para instalar todas as dependências necessárias conforme indicado no arquivo package.json, execute o comando yarn install no terminal.
Configurando o SDK CometChat
Siga os passos abaixo para configurar o SDK CometChat; no final, você deve salvar suas chaves do aplicativo como uma variável de ambiente.
PASSO 1:
Vá ao Painel de Controle da CometChat e crie uma conta.
PASSO 2:
Faça o login no dashboard do CometChat somente depois de registrar-se.
PASSO 3:
Do dashboard, adicione um novo aplicativo chamado Auction.
PASSO 4:
Selecione o aplicativo que você acabou de criar a partir da lista.
PASSO 5:
A partir do Quick Start copie o APP_ID, REGION e AUTH_KEY para o seu arquivo .env. Veja a imagem e o trecho do código.
Substituir os asteriscos com os devidos valores para o REACT_COMET_CHAT.
REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=******************************
O arquivo .env deve ser criado na raiz de seu projeto.
Configurando o script do Hardhat
Abra o arquivo hardhat.config.js na raiz desse projeto e substitua os conteúdos com as seguintes configurações.
require("@nomiclabs/hardhat-waffle");
require('dotenv').config()
module.exports = {
defaultNetwork: "localhost",
networks: {
hardhat: {
},
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
}
}
O script acima instrui o hardhat sobre estas três importantes regras.
- Networks (redes): Este bloco contém as configurações para sua escolha de redes. Na implantação, o hardhat solicitará que você especifique uma rede para o envio de seus contratos inteligentes.
- Solidity: Isto descreve a versão do compilador a ser usada pelo hardhat para compilar seus códigos de contrato inteligentes em bytecodes e abi.
- Paths: Isto simplesmente informa ao hardhat a localização de seus contratos inteligentes e também um local para descarregar a saída do compilador que é a ABI.
Confira este vídeo sobre como construir uma organização autônoma descentralizada.
Você também pode assinar o canal para mais vídeos como esse.
Desenvolvendo o contrato inteligente
Vamos criar um contrato inteligente para este projeto, criando uma nova pasta chamada contracts no diretório src do projeto.
Dentro da pasta contracts, crie um arquivo chamado ‘DappAuction.sol’ que conterá o código que define o comportamento do contrato inteligente. Copie e cole o seguinte código para o arquivo ‘DappAuction.sol’. O código completo é apresentado abaixo.”
//SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract Auction is ERC721URIStorage, ReentrancyGuard {
using Counters for Counters.Counter;
Counters.Counter private totalItems;
address companyAcc;
uint listingPrice = 0.02 ether;
uint royalityFee;
mapping(uint => AuctionStruct) auctionedItem;
mapping(uint => bool) auctionedItemExist;
mapping(string => uint) existingURIs;
mapping(uint => BidderStruct[]) biddersOf;
constructor(uint _royaltyFee) ERC721("Daltonic Tokens", "DAT") {
companyAcc = msg.sender;
royalityFee = _royaltyFee;
}
struct BidderStruct {
address bidder;
uint price;
uint timestamp;
bool refunded;
bool won;
}
struct AuctionStruct {
string name;
string description;
string image;
uint tokenId;
address seller;
address owner;
address winner;
uint price;
bool sold;
bool live;
bool biddable;
uint bids;
uint duration;
}
event AuctionItemCreated(
uint indexed tokenId,
address seller,
address owner,
uint price,
bool sold
);
function getListingPrice() public view returns (uint) {
return listingPrice;
}
function setListingPrice(uint _price) public {
require(msg.sender == companyAcc, "Unauthorized entity");
listingPrice = _price;
}
function changePrice(uint tokenId, uint price) public {
require(
auctionedItem[tokenId].owner == msg.sender,
"Unauthorized entity"
);
require(
getTimestamp(0, 0, 0, 0) > auctionedItem[tokenId].duration,
"Auction still Live"
);
require(price > 0 ether, "Price must be greater than zero");
auctionedItem[tokenId].price = price;
}
function mintToken(string memory tokenURI) internal returns (bool) {
totalItems.increment();
uint tokenId = totalItems.current();
_mint(msg.sender, tokenId);
_setTokenURI(tokenId, tokenURI);
return true;
}
function createAuction(
string memory name,
string memory description,
string memory image,
string memory tokenURI,
uint price
) public payable nonReentrant {
require(price > 0 ether, "Sales price must be greater than 0 ethers.");
require(
msg.value >= listingPrice,
"Price must be up to the listing price."
);
require(mintToken(tokenURI), "Could not mint token");
uint tokenId = totalItems.current();
AuctionStruct memory item;
item.tokenId = tokenId;
item.name = name;
item.description = description;
item.image = image;
item.price = price;
item.duration = getTimestamp(0, 0, 0, 0);
item.seller = msg.sender;
item.owner = msg.sender;
auctionedItem[tokenId] = item;
auctionedItemExist[tokenId] = true;
payTo(companyAcc, listingPrice);
emit AuctionItemCreated(tokenId, msg.sender, address(0), price, false);
}
function offerAuction(
uint tokenId,
bool biddable,
uint sec,
uint min,
uint hour,
uint day
) public {
require(
auctionedItem[tokenId].owner == msg.sender,
"Unauthorized entity"
);
require(
auctionedItem[tokenId].bids == 0,
"Winner should claim prize first"
);
if (!auctionedItem[tokenId].live) {
setApprovalForAll(address(this), true);
IERC721(address(this)).transferFrom(
msg.sender,
address(this),
tokenId
);
}
auctionedItem[tokenId].bids = 0;
auctionedItem[tokenId].live = true;
auctionedItem[tokenId].sold = false;
auctionedItem[tokenId].biddable = biddable;
auctionedItem[tokenId].duration = getTimestamp(sec, min, hour, day);
}
function placeBid(uint tokenId) public payable {
require(
msg.value >= auctionedItem[tokenId].price,
"Insufficient Amount"
);
require(
auctionedItem[tokenId].duration > getTimestamp(0, 0, 0, 0),
"Auction not available"
);
require(auctionedItem[tokenId].biddable, "Auction only for bidding");
BidderStruct memory bidder;
bidder.bidder = msg.sender;
bidder.price = msg.value;
bidder.timestamp = getTimestamp(0, 0, 0, 0);
biddersOf[tokenId].push(bidder);
auctionedItem[tokenId].bids++;
auctionedItem[tokenId].price = msg.value;
auctionedItem[tokenId].winner = msg.sender;
}
function claimPrize(uint tokenId, uint bid) public {
require(
getTimestamp(0, 0, 0, 0) > auctionedItem[tokenId].duration,
"Auction still Live"
);
require(
auctionedItem[tokenId].winner == msg.sender,
"You are not the winner"
);
biddersOf[tokenId][bid].won = true;
uint price = auctionedItem[tokenId].price;
address seller = auctionedItem[tokenId].seller;
auctionedItem[tokenId].winner = address(0);
auctionedItem[tokenId].live = false;
auctionedItem[tokenId].sold = true;
auctionedItem[tokenId].bids = 0;
auctionedItem[tokenId].duration = getTimestamp(0, 0, 0, 0);
uint royality = (price * royalityFee) / 100;
payTo(auctionedItem[tokenId].owner, (price - royality));
payTo(seller, royality);
IERC721(address(this)).transferFrom(address(this), msg.sender, tokenId);
auctionedItem[tokenId].owner = msg.sender;
performRefund(tokenId);
}
function performRefund(uint tokenId) internal {
for (uint i = 0; i < biddersOf[tokenId].length; i++) {
if (biddersOf[tokenId][i].bidder != msg.sender) {
biddersOf[tokenId][i].refunded = true;
payTo(
biddersOf[tokenId][i].bidder,
biddersOf[tokenId][i].price
);
} else {
biddersOf[tokenId][i].won = true;
}
biddersOf[tokenId][i].timestamp = getTimestamp(0, 0, 0, 0);
}
delete biddersOf[tokenId];
}
function buyAuctionedItem(uint tokenId) public payable nonReentrant {
require(
msg.value >= auctionedItem[tokenId].price,
"Insufficient Amount"
);
require(
auctionedItem[tokenId].duration > getTimestamp(0, 0, 0, 0),
"Auction not available"
);
require(!auctionedItem[tokenId].biddable, "Auction only for purchase");
address seller = auctionedItem[tokenId].seller;
auctionedItem[tokenId].live = false;
auctionedItem[tokenId].sold = true;
auctionedItem[tokenId].bids = 0;
auctionedItem[tokenId].duration = getTimestamp(0, 0, 0, 0);
uint royality = (msg.value * royalityFee) / 100;
payTo(auctionedItem[tokenId].owner, (msg.value - royality));
payTo(seller, royality);
IERC721(address(this)).transferFrom(
address(this),
msg.sender,
auctionedItem[tokenId].tokenId
);
auctionedItem[tokenId].owner = msg.sender;
}
function getAuction(uint id) public view returns (AuctionStruct memory) {
require(auctionedItemExist[id], "Auctioned Item not found");
return auctionedItem[id];
}
function getAllAuctions()
public
view
returns (AuctionStruct[] memory Auctions)
{
uint totalItemsCount = totalItems.current();
Auctions = new AuctionStruct[](totalItemsCount);
for (uint i = 0; i < totalItemsCount; i++) {
Auctions[i] = auctionedItem[i + 1];
}
}
function getUnsoldAuction()
public
view
returns (AuctionStruct[] memory Auctions)
{
uint totalItemsCount = totalItems.current();
uint totalSpace;
for (uint i = 0; i < totalItemsCount; i++) {
if (!auctionedItem[i + 1].sold) {
totalSpace++;
}
}
Auctions = new AuctionStruct[](totalSpace);
uint index;
for (uint i = 0; i < totalItemsCount; i++) {
if (!auctionedItem[i + 1].sold) {
Auctions[index] = auctionedItem[i + 1];
index++;
}
}
}
function getMyAuctions()
public
view
returns (AuctionStruct[] memory Auctions)
{
uint totalItemsCount = totalItems.current();
uint totalSpace;
for (uint i = 0; i < totalItemsCount; i++) {
if (auctionedItem[i + 1].owner == msg.sender) {
totalSpace++;
}
}
Auctions = new AuctionStruct[](totalSpace);
uint index;
for (uint i = 0; i < totalItemsCount; i++) {
if (auctionedItem[i + 1].owner == msg.sender) {
Auctions[index] = auctionedItem[i + 1];
index++;
}
}
}
function getSoldAuction()
public
view
returns (AuctionStruct[] memory Auctions)
{
uint totalItemsCount = totalItems.current();
uint totalSpace;
for (uint i = 0; i < totalItemsCount; i++) {
if (auctionedItem[i + 1].sold) {
totalSpace++;
}
}
Auctions = new AuctionStruct[](totalSpace);
uint index;
for (uint i = 0; i < totalItemsCount; i++) {
if (auctionedItem[i + 1].sold) {
Auctions[index] = auctionedItem[i + 1];
index++;
}
}
}
function getLiveAuctions()
public
view
returns (AuctionStruct[] memory Auctions)
{
uint totalItemsCount = totalItems.current();
uint totalSpace;
for (uint i = 0; i < totalItemsCount; i++) {
if (auctionedItem[i + 1].duration > getTimestamp(0, 0, 0, 0)) {
totalSpace++;
}
}
Auctions = new AuctionStruct[](totalSpace);
uint index;
for (uint i = 0; i < totalItemsCount; i++) {
if (auctionedItem[i + 1].duration > getTimestamp(0, 0, 0, 0)) {
Auctions[index] = auctionedItem[i + 1];
index++;
}
}
}
function getBidders(uint tokenId)
public
view
returns (BidderStruct[] memory)
{
return biddersOf[tokenId];
}
function getTimestamp(
uint sec,
uint min,
uint hour,
uint day
) internal view returns (uint) {
return
block.timestamp +
(1 seconds * sec) +
(1 minutes * min) +
(1 hours * hour) +
(1 days * day);
}
function payTo(address to, uint amount) internal {
(bool success, ) = payable(to).call{value: amount}("");
require(success);
}
}
Vamos examinar algumas das especificidades do que está acontecendo no contrato inteligente acima. Os seguintes itens estão disponíveis:
Importação de contratos
Abaixo estão os contratos inteligentes importados da biblioteca openzeppelin:
- Counters: Para acompanhar todos os NFTs na plataforma.
- ERC721: Este é um padrão para tokens não fungíveis na blockchain Ethereum. Ele define um conjunto de funções e eventos que um contrato inteligente que implementa o padrão ERC721 deve ter.
- ERC721URIStorage: Este é um contrato inteligente que armazena o URI (Uniform Resource Identifier ou identificador uniforme de recurso) de um token ERC721.
- ReentrancyGuard: Esta importação mantém nosso contrato inteligente seguro contra ataques de reentrâncias.
Variáveis de estado
- Totalitems: Esta variável contém registros do número de NFTs disponíveis em nosso contrato inteligente.
- CompanyAcc: Contém um registro do endereço da carteira do implantador.
- ListingPrice: Esta contém o preço para criar e listar um NFT na plataforma.
- RoyalityFee: Esta é a porcentagem de royalties que o vendedor de um NFT recebe em cada venda.
Mapeamentos
- AuctionedItem: Contém todos os dados dos NFTs cunhados em nossa plataforma.
- AuctionedItemExist: Utilizado para validar a existência de um NFT.
- ExistingURIs: Detém URIs dos metadados cunhados.
- BiddersOf: Suporta os registros de licitantes para um determinado leilão.
Structs e Eventos
- BidderStruct: Descreve as informações sobre um determinado licitante.
- AuctionStruct: Descreve as informações sobre um determinado item do NFT.
- AuctionItemCreated: Um evento que registra informações sobre o recém-criado NFT.
Funções
- Constructor(): Isto inicializa o contrato inteligente com a conta da empresa, a taxa de royalties estipulada e o nome e símbolo do token.
- GetListingPrice(): Retorna o preço estabelecido para a criação de um NFT na plataforma.
- SetListingPrice(): Usado para atualizar o preço de cunhagem para a criação de um NFT.
- ChangePrice(): Utilizado para modificar o custo de um NFT específico.
- MintToken(): Usado para Criar um novo token.
- CreateAuction(): Usado para Criar um novo leilão usando uma identificação de token cunhado.
- OfferAuction(): Utilizado para a colocação de um item de NFT no mercado.
- PlaceBid(): Usado para licitar em um leilão.
- ClaimPrize(): Usado para transferir um NFT para os licitantes com maior oferta.
- PerformRefund(): Utilizado para reembolsar os licitantes que não surgiram como vencedores em cada leilão.
- BuyAuctionedItem(): Usado para adquirir NFTs vendidos diretamente.
- GetAuction(): Retorna um leilão pelo Id do token.
- GetAllAuctions(): Retorna todos os leilões disponíveis do contrato.
- GetUnsoldAuction() Retorna todos os leilões não vendidos.
- GetSoldAuction(): Retorna todos os leilões vendidos.
- GetMyAuctions(): Retorna todos os leilões pertencentes ao chamador da função.
- GetLiveAuctions(): Retorna todos os leilões listados no mercado.
- GetBidders(): Retorna os licitantes de um leilão específico mediante a identificação do token.
- GetTimestamp(): Retorna um timestamp (carimbo de data) para uma data específica.
- PayTo(): Envia ethers para uma conta específica.
Configurando o Script de Implantação
Navegue até a pasta de scripts e depois até seu arquivo deploy.js e cole o código abaixo nele. Se você não encontrar uma pasta de scripts, crie uma. Crie um arquivo deploy.js e cole o código abaixo nele.
const { ethers } = require('hardhat')
const fs = require('fs')
async function main() {
const royaltyFee = 5
const Contract = await ethers.getContractFactory('Auction')
const contract = await Contract.deploy(royaltyFee)
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
})
Quando executado como um comando Hardhat, o script acima irá implantar o contrato inteligente Auction.sol em sua rede local da blockchain.
Seguindo as instruções acima, abra um terminal que direcione para este projeto e execute os comandos listados abaixo separadamente em dois terminais. Você pode fazer isso diretamente de seu editor no VS Code.
Veja o comando abaixo.
yarn hardhat node # Terminal 1
yarn hardhat run scripts/deploy.js # Terminal 2
Se os comandos anteriores foram executados com sucesso, você deve ver a seguinte atividade em seu terminal. Por favor, veja a imagem abaixo.
Configurando o App Infuria
PASSO 1: Vá até o Infuria e crie uma conta.
PASSO 2: A partir do painel de controle crie um novo projeto.
PASSO 3: Copie o ID do projeto e seu segredo da chave de API para seu arquivo .env no formato abaixo e salve.
Arquivo Env
INFURIA_PID=*************************** INFURIA_API=**********************
REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=******************************
Desenvolvendo uma API de Processamento de Imagem
Precisamos de uma maneira de gerar metadados de uma Imagem que pretendemos fazer de um NFT. O problema é que o JavaScript no navegador não pode nos dar o resultado que pretendemos. Será necessário um script NodeJs para nos ajudar a processar as imagens, gerar metadados, distribuir para o IPFS e retornar o URI do token como uma resposta API. Não há necessidade de muita conversa, deixe-me mostrar-lhe como implementar esta API.
Primeiro, você precisará das seguintes bibliotecas, que já estão instaladas neste projeto, cortesia do comando yarn install que você executou anteriormente.
- Express(): Possibilita a criação de servidores e o compartilhamento de recursos.
- Express-Fileupload(): Permite o upload de arquivos como o upload de uma imagem.
- Cors(): Possibilita o compartilhamento de pedidos de origem cruzada.
- Fs(): Permite o acesso ao sistema de arquivos de nossa máquina local.
- Dotenv(): Permite a gestão das variáveis de ambiente.
- Sharp(): Possibilita o processamento de imagens em diferentes dimensões e extensões.
- Faker(): Possibilita a geração de dados aleatórios e falsos.
- IpfsClient(): Permite o upload de arquivos para o IPFS.
Vamos agora escrever algumas funções essenciais do script que nos ajudarão na conversão de imagens, assim como outras informações como seus nomes, descrições, preços, Ids etc., para seus metadados equivalentes.
Crie uma pasta chamada “api” na raiz do seu projeto. Em seguida, crie um novo arquivo chamado metadata.js dentro dela e cole o código abaixo nele.
Metadata.js File
const sharp = require('sharp')
const { faker } = require('@faker-js/faker')
const ipfsClient = require('ipfs-http-client')
const auth =
'Basic ' +
Buffer.from(process.env.INFURIA_PID + ':' + process.env.INFURIA_API).toString(
'base64',
)
const client = ipfsClient.create({
host: 'ipfs.infura.io',
port: 5001,
protocol: 'https',
headers: {
authorization: auth,
},
})
const attributes = {
weapon: [
'Stick',
'Knife',
'Blade',
'Club',
'Ax',
'Sword',
'Spear',
'Halberd',
],
environment: [
'Space',
'Sky',
'Deserts',
'Forests',
'Grasslands',
'Mountains',
'Oceans',
'Rainforests',
],
rarity: Array.from(Array(6).keys()),
}
const toMetadata = ({ id, name, description, price, image }) => ({
id,
name,
description,
price,
image,
demand: faker.random.numeric({ min: 10, max: 100 }),
attributes: [
{
trait_type: 'Environment',
value: attributes.environment.sort(() => 0.5 - Math.random())[0],
},
{
trait_type: 'Weapon',
value: attributes.weapon.sort(() => 0.5 - Math.random())[0],
},
{
trait_type: 'Rarity',
value: attributes.rarity.sort(() => 0.5 - Math.random())[0],
},
{
display_type: 'date',
trait_type: 'Created',
value: Date.now(),
},
{
display_type: 'number',
trait_type: 'generation',
value: 1,
},
],
})
const toWebp = async (image) => await sharp(image).resize(500).webp().toBuffer()
const uploadToIPFS = async (data) => {
const created = await client.add(data)
return `https://ipfs.io/ipfs/${created.path}`
}
exports.toWebp = toWebp
exports.toMetadata = toMetadata
exports.uploadToIPFS = uploadToIPFS
Agora vamos utilizar estas funções no arquivo principal do NodeJs abaixo.
App.js File
Crie outro script chamado app.js dentro desta pasta API e cole os códigos abaixo; é aqui que residirá a lógica de controle da API.
require('dotenv').config()
const cors = require('cors')
const fs = require('fs').promises
const express = require('express')
const fileupload = require('express-fileupload')
const { toWebp, toMetadata, uploadToIPFS } = require('./metadata')
const app = express()
app.use(cors())
app.use(fileupload())
app.use(express.json())
app.use(express.static('public'))
app.use(express.urlencoded({ extended: true }))
app.post('/process', async (req, res) => {
try {
const name = req.body.name
const description = req.body.description
const price = req.body.price
const image = req.files.image
if (!name || !description || !price || !image) {
return res
.status(400)
.send('name, description, and price must not be empty')
}
let params
await toWebp(image.data).then(async (data) => {
const imageURL = await uploadToIPFS(data)
params = {
id: Date.now(),
name,
description,
price,
image: imageURL,
}
})
fs.writeFile('token.json', JSON.stringify(toMetadata(params)))
.then(() => {
fs.readFile('token.json')
.then(async (data) => {
const metadataURI = await uploadToIPFS(data)
console.log({ ...toMetadata(params), metadataURI })
return res.status(201).json({ ...toMetadata(params), metadataURI })
})
.catch((error) => console.log(error))
})
.catch((error) => console.log(error))
} catch (error) {
console.log(error)
return res.status(400).json({ error })
}
})
app.listen(9000, () => {
console.log('Listen on the port 9000...')
})
A biblioteca IPFS usa o portal Infuria para o upload de arquivos para o IPFS que já configuramos no arquivo .env.
Agora execute node api/app.js no terminal para iniciar o serviço API, como pode ser visto na imagem abaixo.
Importação de chaves privadas para a Metamask
Para usar a Metamask com sua rede local Hardhat, que é representada como Localhost:8545, siga os seguintes passos para configurá-la.
Execute yarn hardhat node
em seu terminal para subir seu servidor blockchain local. Você deve ver uma imagem semelhante à que se encontra abaixo no terminal.
Copie a chave privada da conta em zero(0) e importe-a em sua Metamask. Veja a imagem abaixo.
Agora, você pode repetir os passos acima e importar até três ou quatro contas, dependendo de sua necessidade.
Todos os processos necessários para desenvolver um contrato inteligente pronto para a produção já estão empacotados neste livro, de uma maneira fácil de entender.
Pegue uma cópia do meu livro intitulado "capturando o desenvolvimento de contratos inteligentes".” para se tornar um desenvolvedor de contratos inteligentes sob demanda.
Desenvolvendo o Frontend
Vamos agora usar o React para construir o front end de nosso projeto, utilizando o contrato inteligente e informações relacionadas que foram colocadas na rede e geradas como artefatos (incluindo os bytecodes e ABI). Faremos isso seguindo um processo passo a passo.
Componentes
No diretório src, crie uma nova pasta chamada components* para abrigar todos os componentes do React abaixo.
Componente de Cabeçalho
Agora, crie um componente na pasta de componentes chamado Header.jsx e cole os seguintes códigos abaixo. Os projetos de todos estes componentes foram realizados utilizando a estrutura CSS Tailwind.
import { Link } from 'react-router-dom'
import { connectWallet } from '../services/blockchain'
import { truncate, useGlobalState } from '../store'
const Header = () => {
const [connectedAccount] = useGlobalState('connectedAccount')
return (
<nav className="w-4/5 flex flex-row md:justify-center justify-between items-center py-4 mx-auto">
<div className="md:flex-[0.5] flex-initial justify-center items-center">
<Link to="/" className="text-white">
<span className="px-2 py-1 font-bold text-3xl italic">Dapp</span>
<span className="py-1 font-semibold italic">Auction-NFT</span>
</Link>
</div>
<ul
className="md:flex-[0.5] text-white md:flex
hidden list-none flex-row justify-between
items-center flex-initial"
>
<Link to="/" className="mx-4 cursor-pointer">Market</Link>
<Link to="/collections" className="mx-4 cursor-pointer">Collection</Link>
<Link className="mx-4 cursor-pointer">Artists</Link>
<Link className="mx-4 cursor-pointer">Community</Link>
</ul>
{connectedAccount ? (
<button
className="shadow-xl shadow-black text-white
bg-green-500 hover:bg-green-700 md:text-xs p-2
rounded-full cursor-pointer text-xs sm:text-base"
>
{truncate(connectedAccount, 4, 4, 11)}
</button>
) : (
<button
className="shadow-xl shadow-black text-white
bg-green-500 hover:bg-green-700 md:text-xs p-2
rounded-full cursor-pointer text-xs sm:text-base"
onClick={connectWallet}
>
Connect Wallet
</button>
)}
</nav>
)
}
export default Header
Componente Hero
Em seguida, crie outro componente na pasta de componentes chamado Hero.jsx e cole os seguintes códigos abaixo.
import { toast } from 'react-toastify'
import { BsArrowRightShort } from 'react-icons/bs'
import picture0 from '../assets/images/picture0.png'
import { setGlobalState, useGlobalState } from '../store'
import { loginWithCometChat, signUpWithCometChat } from '../services/chat'
const Hero = () => {
return (
<div className="flex flex-col items-start md:flex-row w-4/5 mx-auto mt-11">
<Banner />
<Bidder />
</div>
)
}
const Bidder = () => (
<div
className="w-full text-white overflow-hidden bg-gray-800 rounded-md shadow-xl
shadow-black md:w-3/5 lg:w-2/5 md:mt-0 font-sans"
>
<img src={picture0} alt="nft" className="object-cover w-full h-60" />
<div
className="shadow-lg shadow-gray-400 border-4 border-[#ffffff36]
flex flex-row justify-between items-center px-3"
>
<div className="p-2">
Current Bid
<div className="font-bold text-center">2.231 ETH</div>
</div>
<div className="p-2">
Auction End
<div className="font-bold text-center">20:10</div>
</div>
</div>
<div
className="bg-green-500 w-full h-[40px] p-2 text-center
font-bold font-mono "
>
Place a Bid
</div>
</div>
)
const Banner = () => {
const [currentUser] = useGlobalState('currentUser')
const handleLogin = async () => {
await toast.promise(
new Promise(async (resolve, reject) => {
await loginWithCometChat()
.then((user) => {
setGlobalState('currentUser', user)
console.log(user)
resolve()
})
.catch((err) => {
console.log(err)
reject()
})
}),
{
pending: 'Signing in...',
success: 'Logged in successful 👌',
error: 'Error, are you signed up? 🤯',
},
)
}
const handleSignup = async () => {
await toast.promise(
new Promise(async (resolve, reject) => {
await signUpWithCometChat()
.then((user) => {
console.log(user)
resolve(user)
})
.catch((err) => {
console.log(err)
reject(err)
})
}),
{
pending: 'Signing up...',
success: 'Signned up successful 👌',
error: 'Error, maybe you should login instead? 🤯',
},
)
}
return (
<div
className="flex flex-col md:flex-row w-full justify-between
items-center mx-auto"
>
<div className="">
<h1 className="text-white font-semibold text-5xl py-1">
Discover, Collect
</h1>
<h1 className="font-semibold text-4xl mb-5 text-white py-1">
and Sell
<span className="text-green-500 px-1">NFTs</span>.
</h1>
<p className="text-white font-light">
More than 100+ NFT available for collect
</p>
<p className="text-white mb-11 font-light">& sell, get your NFT now.</p>
<div className="flex flew-row text-5xl mb-4">
{!currentUser ? (
<div className="flex justify-start items-center space-x-2">
<button
className="text-white text-sm p-2 bg-green-500 rounded-sm w-auto
flex flex-row justify-center items-center shadow-md shadow-gray-700"
onClick={handleLogin}
>
Login Now
</button>
<button
className="text-white text-sm p-2 flex flex-row shadow-md shadow-gray-700
justify-center items-center bg-[#ffffff36] rounded-sm w-auto"
onClick={handleSignup}
>
Signup Now
</button>
</div>
) : (
<button
className="text-white text-sm p-2 bg-green-500 rounded-sm w-auto
flex flex-row justify-center items-center shadow-md shadow-gray-700"
onClick={() => setGlobalState('boxModal', 'scale-100')}
>
Create NFT
<BsArrowRightShort className="font-bold animate-pulse" />
</button>
)}
</div>
<div className="flex items-center justify-between w-3/4 mt-5">
<div>
<p className="text-white font-bold">100k+</p>
<small className="text-gray-300">Auction</small>
</div>
<div>
<p className="text-white font-bold">210k+</p>
<small className="text-gray-300">Rare</small>
</div>
<div>
<p className="text-white font-bold">120k+</p>
<small className="text-gray-300">Artist</small>
</div>
</div>
</div>
</div>
)
}
export default Hero
Componente Artwork
Novamente, crie um componente na pasta de componentes chamado Artworks.jsx e cole os seguintes códigos abaixo.
import { Link } from 'react-router-dom'
import { toast } from 'react-toastify'
import { buyNFTItem } from '../services/blockchain'
import { setGlobalState } from '../store'
import Countdown from './Countdown'
const Artworks = ({ auctions, title, showOffer }) => {
return (
<div className="w-4/5 py-10 mx-auto justify-center">
<p className="text-xl uppercase text-white mb-4">
{title ? title : 'Current Bids'}
</p>
<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 text-white font-mono px-1"
>
{auctions.map((auction, i) => (
<Auction key={i} auction={auction} showOffer={showOffer} />
))}
</div>
</div>
)
}
const Auction = ({ auction, showOffer }) => {
const onOffer = () => {
setGlobalState('auction', auction)
setGlobalState('offerModal', 'scale-100')
}
const onPlaceBid = () => {
setGlobalState('auction', auction)
setGlobalState('bidBox', 'scale-100')
}
const onEdit = () => {
setGlobalState('auction', auction)
setGlobalState('priceModal', 'scale-100')
}
const handleNFTpurchase = async () => {
await toast.promise(
new Promise(async (resolve, reject) => {
await buyNFTItem(auction)
.then(() => resolve())
.catch(() => reject())
}),
{
pending: 'Processing...',
success: 'Purchase successful, will reflect within 30sec 👌',
error: 'Encountered error 🤯',
},
)
}
return (
<div
className="full overflow-hidden bg-gray-800 rounded-md shadow-xl
shadow-black md:w-6/4 md:mt-0 font-sans my-4"
>
<Link to={'/nft/' + auction.tokenId}>
<img
src={auction.image}
alt={auction.name}
className="object-cover w-full h-60"
/>
</Link>
<div
className="shadow-lg shadow-gray-400 border-4 border-[#ffffff36]
flex flex-row justify-between items-center text-gray-300 px-2"
>
<div className="flex flex-col items-start py-2 px-1">
<span>Current Bid</span>
<div className="font-bold text-center">{auction.price} ETH</div>
</div>
<div className="flex flex-col items-start py-2 px-1">
<span>Auction End</span>
<div className="font-bold text-center">
{auction.live && auction.duration > Date.now() ? (
<Countdown timestamp={auction.duration} />
) : (
'00:00:00'
)}
</div>
</div>
</div>
{showOffer ? (
auction.live && Date.now() < auction.duration ? (
<button
className="bg-yellow-500 w-full h-[40px] p-2 text-center
font-bold font-mono"
onClick={onOffer}
>
Auction Live
</button>
) : (
<div className="flex justify-start">
<button
className="bg-red-500 w-full h-[40px] p-2 text-center
font-bold font-mono"
onClick={onOffer}
>
Offer
</button>
<button
className="bg-orange-500 w-full h-[40px] p-2 text-center
font-bold font-mono"
onClick={onEdit}
>
Change
</button>
</div>
)
) : auction.biddable ? (
<button
className="bg-green-500 w-full h-[40px] p-2 text-center
font-bold font-mono"
onClick={onPlaceBid}
disabled={Date.now() > auction.duration}
>
Place a Bid
</button>
) : (
<button
className="bg-red-500 w-full h-[40px] p-2 text-center
font-bold font-mono"
onClick={handleNFTpurchase}
disabled={Date.now() > auction.duration}
>
Buy NFT
</button>
)}
</div>
)
}
export default Artworks
Componente Footer
A seguir, crie um componente na pasta de componentes chamado Footer.jsx e cole os seguintes códigos abaixo.
import React from 'react'
const Footer = () => {
return (
<div className="w-4/5 flex sm:flex-row flex-col justify-between items-center my-4 mx-auto py-5">
<div className="hidden sm:flex flex-1 justify-start items-center space-x-12">
<p className="text-white text-base text-center cursor-pointer">
Market
</p>
<p className="text-white text-base text-center cursor-pointer">
Artist
</p>
<p className="text-white text-base text-center cursor-pointer">
Features
</p>
<p className="text-white text-base text-center cursor-pointer">
Community
</p>
</div>
<p className="text-white text-right text-xs">
©2022 All rights reserved
</p>
</div>
)
}
export default Footer
Outros Componentes
A seguir estão os componentes que suportam a funcionalidade total do restante deste aplicativo.
Componente Countdown
Este componente é responsável pela renderização de uma contagem regressiva do tempo para todos os NFTs. Veja os códigos abaixo.
import { useState, useEffect } from 'react'
const Countdown = ({ timestamp }) => {
const [timeLeft, setTimeLeft] = useState(timestamp - Date.now())
useEffect(() => {
const interval = setInterval(() => {
setTimeLeft(timestamp - Date.now())
}, 1000)
return () => clearInterval(interval)
}, [timestamp])
const days = Math.floor(timeLeft / (1000 * 60 * 60 * 24))
const hours = Math.floor(
(timeLeft % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60),
)
const minutes = Math.floor((timeLeft % (1000 * 60 * 60)) / (1000 * 60))
const seconds = Math.floor((timeLeft % (1000 * 60)) / 1000)
return Date.now() > timestamp ? (
'00:00:00'
) : (
<div>
{days}d : {hours}h : {minutes}m : {seconds}s
</div>
)
}
export default Countdown
O componente Empty (vazio)
Este componente é responsável pela exibição de um pequeno texto informando aos usuários que não há NFTs na plataforma. Veja o código de exemplo abaixo para implementação.
const Empty = () => {
return (
<div className="w-4/5 h-48 py-10 mx-auto justify-center">
<h4 className="text-xl capitalize text-white mb-4">Nothing here bring some artworks</h4>
</div>
)
}
export default Empty
Criando um NFT
Para escrever um componente Create NFT, use os seguintes códigos. Este será um modal que aceita uma imagem, um título, uma descrição e um preço antes de submetê-lo à blockchain.
Antes de serem armazenados na blockchain, os dados coletados de um formulário são enviados para uma API NodeJs, que os converte em metadados e os implanta no IPFS.
Em seguida, na pasta de componentes, crie um novo arquivo chamado "CreateNFT.jsx" e cole nele o seguinte código.
Oferecendo NFTs no Mercado
Este componente é responsável por oferecer novos itens em tempo real no mercado. Usando um formulário, ele aceita um tempo de duração para o qual você pretende ter seu NFT em oferta no mercado. Uma vez expirada esta linha de tempo, o NFT desaparecerá do mercado. Veja os códigos.
Dar Lances nos Leilões
Este componente permite que um usuário dê um lance e participe de um leilão de NFT. Isto é realizado através do uso de um modal que recebe o preço que um usuário pretende licitar, se estiver dentro do limite do tempo para licitação. Veja os códigos aqui.
Changing an NFT price
Este componente permite ao proprietário de um NFT alterar o preço de um NFT que não esteja atualmente sendo negociado no mercado. Aceitar um novo preço a partir de um formulário e enviá-lo para a blockchain possibilita isso. Veja os códigos.
O Componente Chat
Finalmente, para os componentes, há um componente de chat (bate-papo) que é controlado pelo SDK CometChat. Veja os códigos.
As Páginas
Este aplicativo tem cerca de três exibições ou páginas; vamos organizar todos os componentes acima em suas respectivas exibições usando os passos abaixo. Primeiro, crie uma pasta chamada views no diretório src e crie as páginas a serem discutidas em breve.
A Página Home
A página Home combina dois componentes principais: os componentes Hero e Artworks. Veja os códigos abaixo.
import Artworks from '../components/Artworks'
import Empty from '../components/Empty'
import Hero from '../components/Hero'
import { useGlobalState } from '../store'
const Home = () => {
const [auctions] = useGlobalState('auctions')
return (
<div>
<Hero />
{auctions.length > 0 ? <Artworks auctions={auctions} /> : <Empty />}
</div>
)
}
export default Home
A Página Collections
Esta página exibe todos os NFTs de propriedade de um usuário específico. Ela habilita um usuário a administrar um NFT, como por exemplo, se deve ou não oferecê-lo no mercado ou alterar seu preço. Veja os códigos mostrados abaixo.
import { useEffect } from 'react'
import Empty from '../components/Empty'
import { useGlobalState } from '../store'
import Artworks from '../components/Artworks'
import { loadCollections } from '../services/blockchain'
const Collections = () => {
const [collections] = useGlobalState('collections')
useEffect(async () => {
await loadCollections()
})
return (
<div>
{collections.length > 0 ? (
<Artworks title="Your Collections" auctions={collections} showOffer />
) : (
<Empty />
)}
</div>
)
}
export default Collections
A Página NFT
Finalmente, esta página contém o componente chat, assim como outros componentes importantes, como mostrado no código abaixo.
import { useEffect } from 'react'
import Chat from '../components/Chat'
import { toast } from 'react-toastify'
import Identicons from 'react-identicons'
import { useNavigate, useParams } from 'react-router-dom'
import Countdown from '../components/Countdown'
import { setGlobalState, truncate, useGlobalState } from '../store'
import {
buyNFTItem,
claimPrize,
getBidders,
loadAuction,
} from '../services/blockchain'
import { createNewGroup, getGroup, joinGroup } from '../services/chat'
const Nft = () => {
const { id } = useParams()
const [group] = useGlobalState('group')
const [bidders] = useGlobalState('bidders')
const [auction] = useGlobalState('auction')
const [currentUser] = useGlobalState('currentUser')
const [connectedAccount] = useGlobalState('connectedAccount')
useEffect(async () => {
await loadAuction(id)
await getBidders(id)
await getGroup(`pid_${id}`)
.then((group) => setGlobalState('group', group))
.catch((error) => console.log(error))
}, [])
return (
<>
<div
className="grid sm:flex-row md:flex-row lg:grid-cols-2 gap-6
md:gap-4 lg:gap-3 py-2.5 text-white font-sans capitalize
w-4/5 mx-auto mt-5 justify-between items-center"
>
<div
className=" text-white h-[400px] bg-gray-800 rounded-md shadow-xl
shadow-black md:w-4/5 md:items-center lg:w-4/5 md:mt-0"
>
<img
src={auction?.image}
alt={auction?.name}
className="object-contain w-full h-80 mt-10"
/>
</div>
<div className="">
<Details auction={auction} account={connectedAccount} />
{bidders.length > 0 ? (
<Bidders bidders={bidders} auction={auction} />
) : null}
<CountdownNPrice auction={auction} />
<ActionButton auction={auction} account={connectedAccount} />
</div>
</div>
<div className="w-4/5 mx-auto">
{currentUser ? <Chat id={id} group={group} /> : null}
</div>
</>
)
}
const Details = ({ auction, account }) => (
<div className="py-2">
<h1 className="font-bold text-lg mb-1">{auction?.name}</h1>
<p className="font-semibold text-sm">
<span className="text-green-500">
@
{auction?.owner == account
? 'you'
: auction?.owner
? truncate(auction?.owner, 4, 4, 11)
: ''}
</span>
</p>
<p className="text-sm py-2">{auction?.description}</p>
</div>
)
const Bidders = ({ bidders, auction }) => {
const handlePrizeClaim = async (id) => {
await toast.promise(
new Promise(async (resolve, reject) => {
await claimPrize({ tokenId: auction?.tokenId, id })
.then(() => resolve())
.catch(() => reject())
}),
{
pending: 'Processing...',
success: 'Price claim successful, will reflect within 30sec 👌',
error: 'Encountered error 🤯',
},
)
}
return (
<div className="flex flex-col">
<span>Top Bidders</span>
<div className="h-[calc(100vh_-_40.5rem)] overflow-y-auto">
{bidders.map((bid, i) => (
<div key={i} className="flex justify-between items-center">
<div className="flex justify-start items-center my-1 space-x-1">
<Identicons
className="h-5 w-5 object-contain bg-gray-800 rounded-full"
size={18}
string={bid.bidder}
/>
<span className="font-medium text-sm mr-3">
{truncate(bid.bidder, 4, 4, 11)}
</span>
<span className="text-green-400 font-medium text-sm">
{bid.price} ETH
</span>
</div>
{bid.bidder == auction?.winner &&
!bid.won &&
Date.now() > auction?.duration ? (
<button
type="button"
className="shadow-sm shadow-black text-white
bg-green-500 hover:bg-green-700 md:text-xs p-1
rounded-sm text-sm cursor-pointer font-light"
onClick={() => handlePrizeClaim(i)}
>
Claim Prize
</button>
) : null}
</div>
))}
</div>
</div>
)
}
const CountdownNPrice = ({ auction }) => {
return (
<div className="flex justify-between items-center py-5 ">
<div>
<span className="font-bold">Current Price</span>
<p className="text-sm font-light">{auction?.price}ETH</p>
</div>
<div className="lowercase">
<span className="font-bold">
{auction?.duration > Date.now() ? (
<Countdown timestamp={auction?.duration} />
) : (
'00:00:00'
)}
</span>
</div>
</div>
)
}
const ActionButton = ({ auction, account }) => {
const [group] = useGlobalState('group')
const [currentUser] = useGlobalState('currentUser')
const navigate = useNavigate()
const onPlaceBid = () => {
setGlobalState('auction', auction)
setGlobalState('bidBox', 'scale-100')
}
const handleNFTpurchase = async () => {
await toast.promise(
new Promise(async (resolve, reject) => {
await buyNFTItem(auction)
.then(() => resolve())
.catch(() => reject())
}),
{
pending: 'Processing...',
success: 'Purchase successful, will reflect within 30sec 👌',
error: 'Encountered error 🤯',
},
)
}
const handleCreateGroup = async () => {
if (!currentUser) {
navigate('/')
toast.warning('You need to login or sign up first.')
return
}
await toast.promise(
new Promise(async (resolve, reject) => {
await createNewGroup(`pid_${auction?.tokenId}`, auction?.name)
.then((gp) => {
setGlobalState('group', gp)
resolve(gp)
})
.catch((error) => reject(new Error(error)))
}),
{
pending: 'Creating...',
success: 'Group created 👌',
error: 'Encountered error 🤯',
},
)
}
const handleJoineGroup = async () => {
if (!currentUser) {
navigate('/')
toast.warning('You need to login or sign up first.')
return
}
await toast.promise(
new Promise(async (resolve, reject) => {
await joinGroup(`pid_${auction?.tokenId}`)
.then((gp) => {
setGlobalState('group', gp)
resolve(gp)
})
.catch((error) => reject(new Error(error)))
}),
{
pending: 'Joining...',
success: 'Group Joined 👌',
error: 'Encountered error 🤯',
},
)
}
return auction?.owner == account ? (
<div className="flex justify-start items-center space-x-2 mt-2">
{!group ? (
<button
type="button"
className="shadow-sm shadow-black text-white
bg-red-500 hover:bg-red-700 md:text-xs p-2.5
rounded-sm cursor-pointer font-light"
onClick={handleCreateGroup}
>
Create Group
</button>
) : null}
</div>
) : (
<div className="flex justify-start items-center space-x-2 mt-2">
{!group?.hasJoined ? (
<button
type="button"
className="shadow-sm shadow-black text-white
bg-gray-500 hover:bg-gray-700 md:text-xs p-2.5
rounded-sm cursor-pointer font-light"
onClick={handleJoineGroup}
>
Join Group
</button>
) : null}
{auction?.biddable && auction?.duration > Date.now() ? (
<button
type="button"
className="shadow-sm shadow-black text-white
bg-gray-500 hover:bg-gray-700 md:text-xs p-2.5
rounded-sm cursor-pointer font-light"
onClick={onPlaceBid}
>
Place a Bid
</button>
) : null}
{!auction?.biddable && auction?.duration > Date.now() ? (
<button
type="button"
className="shadow-sm shadow-black text-white
bg-red-500 hover:bg-red-700 md:text-xs p-2.5
rounded-sm cursor-pointer font-light"
onClick={handleNFTpurchase}
>
Buy NFT
</button>
) : null}
</div>
)
}
export default Nft
Atualizando o Arquivo App.jsx
Atualizar o arquivo App com os códigos abaixo para juntar todos os componentes e páginas.
import Nft from './views/Nft'
import Home from './views/Home'
import Header from './components/Header'
import Footer from './components/Footer'
import { useEffect, useState } from 'react'
import PlaceBid from './components/PlaceBid'
import Collections from './views/Collections'
import CreateNFT from './components/CreateNFT'
import { ToastContainer } from 'react-toastify'
import { Route, Routes } from 'react-router-dom'
import { isWallectConnected, loadAuctions } from './services/blockchain'
import { setGlobalState, useGlobalState } from './store'
import OfferItem from './components/OfferItem'
import ChangePrice from './components/ChangePrice'
import { checkAuthState } from './services/chat'
function App() {
const [loaded, setLoaded] = useState(false)
const [auction] = useGlobalState('auction')
useEffect(async () => {
await isWallectConnected()
await loadAuctions().finally(() => setLoaded(true))
await checkAuthState()
.then((user) => setGlobalState('currentUser', user))
.catch((error) => setGlobalState('currentUser', null))
console.log('Blockchain Loaded')
}, [])
return (
<div
className="min-h-screen bg-gradient-to-t from-gray-800 bg-repeat
via-[#25bd9c] to-gray-900 bg-center subpixel-antialiased"
>
<Header />
{loaded ? (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/collections" element={<Collections />} />
<Route path="/nft/:id" element={<Nft />} />
</Routes>
) : null}
<CreateNFT />
{auction ? (
<>
<PlaceBid />
<OfferItem />
<ChangePrice />
</>
) : null}
<Footer />
<ToastContainer
position="bottom-center"
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
theme="dark"
/>
</div>
)
}
export default App
Atualizando os arquivos Index.jsx e CSS
Use os códigos abaixo para atualizar os arquivos index.jsx e index.css, respectivamente.
@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;
}
.gradient-bg-hero {
background-color: #151c25;
background-image: radial-gradient(
at 0% 0%,
hsl(302deg 25% 18%) 0,
transparent 50%
),
radial-gradient(at 50% 0%, hsl(0deg 39% 30%) 0, transparent 50%),
radial-gradient(at 100% 0%, hsla(339, 49%, 30%, 1) 0, transparent 50%);
}
.gradient-bg-artworks {
background-color: #0f0e13;
background-image: radial-gradient(
at 50% 50%,
hsl(302deg 25% 18%) 0,
transparent 50%
),
radial-gradient(at 0% 0%, hsla(253, 16%, 7%, 1) 0, transparent 50%),
radial-gradient(at 50% 50%, hsla(339, 39%, 25%, 1) 0, transparent 50%);
}
.gradient-bg-footer {
background-color: #151c25;
background-image: radial-gradient(
at 0% 100%,
hsl(0deg 39% 30%) 0,
transparent 53%
),
radial-gradient(at 50% 150%, hsla(339, 49%, 30%, 1) 0, transparent 50%);
}
.text-gradient {
background: -webkit-linear-gradient(#eee, #333);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.lds-dual-ring {
display: inline-block;
}
.lds-dual-ring:after {
content: ' ';
display: block;
width: 64px;
height: 64px;
margin: 8px;
border-radius: 50%;
border: 6px solid #fff;
border-color: #fff transparent #fff transparent;
animation: lds-dual-ring 1.2s linear infinite;
}
@keyframes lds-dual-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@tailwind base;
@tailwind components;
@tailwind utilities;
import './index.css'
import App from './App'
import React from 'react'
import ReactDOM from 'react-dom'
import 'react-toastify/dist/ReactToastify.css'
import { initCometChat } from './services/chat'
import { BrowserRouter } from 'react-router-dom'
initCometChat().then(() => {
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root'),
)
})
Adicionando os Serviços do App
Neste aplicativo, temos dois serviços: chat e blockchain, como mostrado nos códigos abaixo. Basta criar uma nova pasta chamada services no diretório src e colocar os seguintes arquivos nela usando os códigos abaixo.
Chat Services
import { CometChat } from '@cometchat-pro/chat'
import { getGlobalState } from '../store'
const 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 = CONSTANTS.APP_ID
const region = 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 () => {
const authKey = CONSTANTS.Auth_Key
const UID = getGlobalState('connectedAccount')
return new Promise(async (resolve, reject) => {
await CometChat.login(UID, authKey)
.then((user) => resolve(user))
.catch((error) => reject(error))
})
}
const signUpWithCometChat = async () => {
const authKey = CONSTANTS.Auth_Key
const UID = getGlobalState('connectedAccount')
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(() => resolve())
.catch(() => reject())
})
}
const checkAuthState = async () => {
return new Promise(async (resolve, reject) => {
await CometChat.getLoggedinUser()
.then((user) => resolve(user))
.catch((error) => reject(error))
})
}
const createNewGroup = async (GUID, groupName) => {
const groupType = CometChat.GROUP_TYPE.PUBLIC
const password = ''
const group = new CometChat.Group(GUID, groupName, groupType, password)
return new Promise(async (resolve, reject) => {
await CometChat.createGroup(group)
.then((group) => resolve(group))
.catch((error) => reject(error))
})
}
const getGroup = async (GUID) => {
return new Promise(async (resolve, reject) => {
await CometChat.getGroup(GUID)
.then((group) => resolve(group))
.catch((error) => reject(error))
})
}
const joinGroup = async (GUID) => {
const groupType = CometChat.GROUP_TYPE.PUBLIC
const password = ''
return new Promise(async (resolve, reject) => {
await CometChat.joinGroup(GUID, groupType, password)
.then((group) => resolve(group))
.catch((error) => reject(error))
})
}
const getMessages = async (UID) => {
const limit = 30
const messagesRequest = new CometChat.MessagesRequestBuilder()
.setGUID(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 sendMessage = async (receiverID, messageText) => {
const receiverType = CometChat.RECEIVER_TYPE.GROUP
const textMessage = new CometChat.TextMessage(
receiverID,
messageText,
receiverType,
)
return new Promise(async (resolve, reject) => {
await CometChat.sendMessage(textMessage)
.then((message) => resolve(message))
.catch((error) => reject(error))
})
}
const listenForMessage = async (listenerID) => {
return new Promise(async (resolve, reject) => {
CometChat.addMessageListener(
listenerID,
new CometChat.MessageListener({
onTextMessageReceived: (message) => resolve(message),
}),
)
})
}
export {
initCometChat,
loginWithCometChat,
signUpWithCometChat,
logOutWithCometChat,
getMessages,
sendMessage,
checkAuthState,
createNewGroup,
getGroup,
joinGroup,
listenForMessage,
}
Blockchain Service
import abi from '../abis/src/contracts/Auction.sol/Auction.json'
import address from '../abis/contractAddress.json'
import { getGlobalState, setGlobalState } from '../store'
import { ethers } from 'ethers'
import { checkAuthState, 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 connectedAccount = getGlobalState('connectedAccount')
if (connectedAccount) {
const provider = new ethers.providers.Web3Provider(ethereum)
const signer = provider.getSigner()
const contract = new ethers.Contract(ContractAddress, ContractAbi, signer)
return contract
} else {
return getGlobalState('contract')
}
}
const isWallectConnected = async () => {
try {
if (!ethereum) return alert('Please install Metamask')
const accounts = await ethereum.request({ method: 'eth_accounts' })
setGlobalState('connectedAccount', accounts[0]?.toLowerCase())
window.ethereum.on('chainChanged', (chainId) => {
window.location.reload()
})
window.ethereum.on('accountsChanged', async () => {
setGlobalState('connectedAccount', accounts[0]?.toLowerCase())
await isWallectConnected()
await loadCollections()
await logOutWithCometChat()
await checkAuthState()
.then((user) => setGlobalState('currentUser', user))
.catch((error) => setGlobalState('currentUser', null))
})
if (accounts.length) {
setGlobalState('connectedAccount', accounts[0]?.toLowerCase())
} else {
alert('Please connect wallet.')
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]?.toLowerCase())
} catch (error) {
reportError(error)
}
}
const createNftItem = async ({
name,
description,
image,
metadataURI,
price,
}) => {
try {
if (!ethereum) return alert('Please install Metamask')
const connectedAccount = getGlobalState('connectedAccount')
const contract = await getEthereumContract()
tx = await contract.createAuction(
name,
description,
image,
metadataURI,
toWei(price),
{
from: connectedAccount,
value: toWei(0.02),
},
)
await tx.wait()
await loadAuctions()
} catch (error) {
reportError(error)
}
}
const updatePrice = async ({ tokenId, price }) => {
try {
if (!ethereum) return alert('Please install Metamask')
const connectedAccount = getGlobalState('connectedAccount')
const contract = await getEthereumContract()
tx = await contract.changePrice(tokenId, toWei(price), {
from: connectedAccount,
})
await tx.wait()
await loadAuctions()
} catch (error) {
reportError(error)
}
}
const offerItemOnMarket = async ({
tokenId,
biddable,
sec,
min,
hour,
day,
}) => {
try {
if (!ethereum) return alert('Please install Metamask')
const connectedAccount = getGlobalState('connectedAccount')
const contract = await getEthereumContract()
tx = await contract.offerAuction(tokenId, biddable, sec, min, hour, day, {
from: connectedAccount,
})
await tx.wait()
await loadAuctions()
} catch (error) {
reportError(error)
}
}
const buyNFTItem = async ({ tokenId, price }) => {
try {
if (!ethereum) return alert('Please install Metamask')
const connectedAccount = getGlobalState('connectedAccount')
const contract = await getEthereumContract()
tx = await contract.buyAuctionedItem(tokenId, {
from: connectedAccount,
value: toWei(price),
})
await tx.wait()
await loadAuctions()
await loadAuction(tokenId)
} catch (error) {
reportError(error)
}
}
const bidOnNFT = async ({ tokenId, price }) => {
try {
if (!ethereum) return alert('Please install Metamask')
const connectedAccount = getGlobalState('connectedAccount')
const contract = await getEthereumContract()
tx = await contract.placeBid(tokenId, {
from: connectedAccount,
value: toWei(price),
})
await tx.wait()
await getBidders(tokenId)
await loadAuction(tokenId)
} catch (error) {
reportError(error)
}
}
const claimPrize = async ({ tokenId, id }) => {
try {
if (!ethereum) return alert('Please install Metamask')
const connectedAccount = getGlobalState('connectedAccount')
const contract = await getEthereumContract()
tx = await contract.claimPrize(tokenId, id, {
from: connectedAccount,
})
await tx.wait()
await getBidders(tokenId)
} catch (error) {
reportError(error)
}
}
const loadAuctions = async () => {
try {
if (!ethereum) return alert('Please install Metamask')
const contract = await getEthereumContract()
const auctions = await contract.getLiveAuctions()
setGlobalState('auctions', structuredAuctions(auctions))
setGlobalState(
'auction',
structuredAuctions(auctions).sort(() => 0.5 - Math.random())[0],
)
} catch (error) {
reportError(error)
}
}
const loadAuction = async (id) => {
try {
if (!ethereum) return alert('Please install Metamask')
const contract = await getEthereumContract()
const auction = await contract.getAuction(id)
setGlobalState('auction', structuredAuctions([auction])[0])
} catch (error) {
reportError(error)
}
}
const getBidders = async (id) => {
try {
if (!ethereum) return alert('Please install Metamask')
const contract = await getEthereumContract()
const bidders = await contract.getBidders(id)
setGlobalState('bidders', structuredBidders(bidders))
} catch (error) {
reportError(error)
}
}
const loadCollections = async () => {
try {
if (!ethereum) return alert('Please install Metamask')
const connectedAccount = getGlobalState('connectedAccount')
const contract = await getEthereumContract()
const collections = await contract.getMyAuctions({ from: connectedAccount })
setGlobalState('collections', structuredAuctions(collections))
} catch (error) {
reportError(error)
}
}
const structuredAuctions = (auctions) =>
auctions
.map((auction) => ({
tokenId: auction.tokenId.toNumber(),
owner: auction.owner.toLowerCase(),
seller: auction.seller.toLowerCase(),
winner: auction.winner.toLowerCase(),
name: auction.name,
description: auction.description,
duration: Number(auction.duration + '000'),
image: auction.image,
price: fromWei(auction.price),
biddable: auction.biddable,
sold: auction.sold,
live: auction.live,
}))
.reverse()
const structuredBidders = (bidders) =>
bidders
.map((bidder) => ({
timestamp: Number(bidder.timestamp + '000'),
bidder: bidder.bidder.toLowerCase(),
price: fromWei(bidder.price),
refunded: bidder.refunded,
won: bidder.won,
}))
.sort((a, b) => b.price - a.price)
const reportError = (error) => {
console.log(error.message)
throw new Error('No ethereum object.')
}
export {
isWallectConnected,
connectWallet,
createNftItem,
loadAuctions,
loadAuction,
loadCollections,
offerItemOnMarket,
buyNFTItem,
bidOnNFT,
getBidders,
claimPrize,
updatePrice,
}
O Store
O store é um serviço de gerenciamento de estado incluído neste aplicativo. É aqui que todos os dados extraídos da blockchain são mantidos. Para replicar, crie uma pasta store dentro do diretório src. Em seguida, dentro desta pasta, crie um arquivo chamado index.jsx e cole nele os códigos abaixo.
import { createGlobalState } from 'react-hooks-global-state'
const { getGlobalState, useGlobalState, setGlobalState } = createGlobalState({
boxModal: 'scale-0',
bidBox: 'scale-0',
offerModal: 'scale-0',
priceModal: 'scale-0',
connectedAccount: '',
collections: [],
bidders: [],
auctions: [],
auction: null,
currentUser: null,
group: null,
})
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 convertToSeconds = (minutes, hours, days) => {
const seconds = minutes * 60 + hours * 3600 + days * 86400
const timestamp = new Date().getTime()
return timestamp + seconds
}
export {
getGlobalState,
useGlobalState,
setGlobalState,
truncate,
convertToSeconds,
}
Agora inicie o aplicativo executando yarn start em outro terminal para ver o resultado no terminal. Se você tiver qualquer problema para replicar este projeto, você pode mandar uma pergunta em nosso canal do discord.
Você também pode ver este vídeo para aprender mais sobre como construir um mercado NFT, desde o projeto até a implantação.
Parabéns. É assim que se constrói um Mercado de NFT usando React, Solidity e CometChat.
Conclusão
Concluindo, a construção de um site de leilão de NFT com React, Solidity e CometChat requer uma combinação de habilidades de desenvolvimento de front-end e back-end.
Ao utilizar essas ferramentas em conjunto, é possível criar um site de leilão de NFT totalmente funcional que seja seguro, escalável e de fácil utilização.
Se você estiver pronto para mergulhar mais profundamente no desenvolvimento da web3, agende comigo aulas particulares de web3 para acelerar seu aprendizado de web3.
Dito isto, até a próxima, e tenha um ótimo dia!
Sobre o Autor
Gospel Darlington é um desenvolvedor de blockchain full-stack com mais de 6 anos de experiência na indústria de desenvolvimento de software.
Ao combinar Desenvolvimento de Software, escrita e ensino, ele demonstra como construir aplicativos descentralizados em redes de blockchain compatíveis com EVM.
Suas stacks incluem JavaScript, React, Vue, Angular, Node, React Native, NextJs, Solidity e muito mais.
Para mais informações sobre ele, acesse e siga sua página no Twitter, Github, LinkedIn ou pelo website.
Esse artigo foi traduzido por Fátima Lima. Seu original pode ser lido aqui.
Latest comments (0)