WEB3DEV

Cover image for Como construir um site de leilão de NFT com React, Solidity e CometChat
Fatima Lima
Fatima Lima

Posted on

Como construir um site de leilão de NFT com React, Solidity e CometChat

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"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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

Image description

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.

Image description

PASSO 2:

Faça o login no dashboard do CometChat somente depois de registrar-se.

Image description

PASSO 3:

Do dashboard, adicione um novo aplicativo chamado Auction.

Image description

Image description

PASSO 4:

Selecione o aplicativo que você acabou de criar a partir da lista.

Image description

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.

Image description

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=******************************
Enter fullscreen mode Exit fullscreen mode

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

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

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.

Image description

Image description

Configurando o App Infuria

PASSO 1: Vá até o Infuria e crie uma conta.

Image description

PASSO 2: A partir do painel de controle crie um novo projeto.

Image description

Image description

PASSO 3: Copie o ID do projeto e seu segredo da chave de API para seu arquivo .env no formato abaixo e salve.

Image description

Arquivo Env

INFURIA_PID=*************************** INFURIA_API=**********************
REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=******************************
Enter fullscreen mode Exit fullscreen mode

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

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...')
})
Enter fullscreen mode Exit fullscreen mode

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.

Image description

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.

Image description

Copie a chave privada da conta em zero(0) e importe-a em sua Metamask. Veja a imagem abaixo.

Image description

Image description

Image description

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.

Image description

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

Image description

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

Componente Hero

Image description

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

Componente Artwork

Image description

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

Componente Footer

Image description

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">
        &copy;2022 All rights reserved
      </p>
    </div>
  )
}

export default Footer
Enter fullscreen mode Exit fullscreen mode

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

O componente Empty (vazio)

Image description

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

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

Image description

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

A Página Collections

Image description

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

A Página NFT

Image description

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

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

Enter fullscreen mode Exit fullscreen mode

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'),
  )
})

Enter fullscreen mode Exit fullscreen mode

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,
}

Enter fullscreen mode Exit fullscreen mode

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,
}
Enter fullscreen mode Exit fullscreen mode

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,
}
Enter fullscreen mode Exit fullscreen mode

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.

Oldest comments (0)