WEB3DEV Español

Cover image for Cómo construir un sitio de subastas NFT con: React, Solidity y CometChat
Hector
Hector

Posted on

Cómo construir un sitio de subastas NFT con: React, Solidity y CometChat

Image description

Esto es lo que tú construirás

Este artículo es una traducción de Darlington Gospel, hecha por Héctor Botero. Puedes encontrar el artículo original aquí.
Sería genial escucharte en nuestro Discord, puedes contarnos tus ideas, comentarios, sugerencias y dejarnos saber lo que necesitas.
Si prefieres puedes escribirnos a @web3dev_eshttps://twitter.com/web3dev_es en Twitter.

Introducción

Bienvenido a este tutorial en cómo construir un sitio de subastas usando: React, Solidity y CometChat. En este tutorial, te vamos a guiar en las etapas de la creación de un mercado descentralizado para comprar y vender tokens no-fungibles. Usaremos React para el front end, Solidity para el desarrollo del contrato inteligente y CometChat para los mensajes y notificaciones en tiempo real. Al final de este tutorial, tendrás una plataforma completa de subastas de NFT, lista para entrar en funcionamiento en la blockchain Ethereum.

Prerrequisitos

Para seguir este tutorial, necesitarás tener instalados los siguientes items en tu computadora. NodeJs es no negociable, los otros pueden ser instalados siguiendo esta guía, así que asegúrate que esté instalado y ejecutado.

  • NodeJs
  • React
  • Solidity
  • Tailwind
  • CometChat SDK
  • Hardhat
  • EtherJs
  • Metamas
  • Yarn

Instalando las dependencias

Clona el kit de comienzo usando el siguiente comando, debajo de la carpeta de tus proyectos:

git clone https://github.com/Daltonic/tailwind_ethers_starter_kit <PROJECT_NAME>

cd <PROJECT_NAME>

Luego, abre el proyecto en VS Code o en tu editor de códigos favorito. Ubica el archivo package.json y actualízalo con los siguientes códigos:

{
 "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 las dependencias requeridas, como se indicó en el archivo package.json, ejecuta el comando yarn install en el terminal.

Configurando CometChat SDK

Image description

Sigue los siguientes pasos para configurar CometChat SDK; al final, debes guardar las llaves de tu aplicación en un entorno variable.

Paso 1:

Ve a tablero principal de CometChat y crea una cuenta:

Image description

Paso 2:

Inicia sesión en el tablero principal de CometChat, luego de registrarte.

Image description

Paso 3:

Desde el tablero principal, añade una nueva aplicación llamada Auction.

Image description

Image description

Paso 4:

Selecciona la app que has creado, desde la lista:

Image description

Paso 5:

Desde Quick Start, copia APP_ID, REGION y AUTH_KEY a tu archivo .env. Mira el fragmento de la imagen y del código

Image description

Reemplaza las llaves placeholder REACT_COMET_CHAT con los valores apropiados.

REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=******************************

El archivo .env debería crearse en el directorio principal de tu proyecto.

Configurando el script Hardhat

Abre el archivo hardhat.configure en el directorio principal de este proyecto y reemplázalo con los contenidos de la siguiente configuración:

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

El script de arriba le da instrucciones a Hardhat sobre las siguientes tres reglas:

  • Redes: Este bloque contiene las configuraciones de la elección de tu red. En el despliegue, hardhat necesitará que tú especifiques una red para enviar tus contratos inteligentes.
  • Solidity: Esto describe la versión de la compilación a ser usada por hardhat, para compilar los códigos de tus contratos inteligentes en bytecodes y abi.
  • Paths: Esto simplemente informa a hardhat, la ubicación de la locación de tus contratos inteligentes y, además, en lugar para tirar la salida de lo compilado, lo cual es tu ABI.

Revisa este video sobre cómo construir una organización autónoma descentralizada.

También puedes suscribirte al canal para más videos como este.

Desarrollando el Contrato Inteligente

Vamos a crear un contrato inteligente para este proyecto, creando una nueva carpeta llamada contracts en el directorio src del proyecto.

Dentro de la carpeta contracts, crea un archivo llamado DappAuction.sol el cual contendrá el código que define el comportamiento del contrato inteligente. Copia y pega el siguiente código en el archivo DappAuction.sol. El código completo está debajo:

//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 a ver alguna de las especificaciones sobre lo que está sucediendo en el contrato inteligente de arriba. Los siguientes items están disponibles:

Importación del Contrato

A continuación, los siguientes son contratos inteligentes importados de la biblioteca openzeppelin:

  • Counters: Para mantener rastreado todos los NFT en la plataforma.
  • ERC721: Esto es estándar para los tokens no-fungibles en el blockchain Ethereum. Define un conjunto de funciones y eventos que un contrato inteligente que implementa el estándar ERC721 que debería tener.
  • ReentrancyGuard: Este importe mantiene nuestros contratos inteligentes seguros contra los ataques de reentrada.

Variables de estado

  • Totalitems: Esta variable lleva los registros de los números de NFT disponibles en nuestro contrato inteligente.
  • CompanyAcc: Esto contiene un registro de la dirección de la cartera desplegada.
  • ListingPrice: Esto contiene el precio para crear y hacer la lista de un NFT en la plataforma.
  • RoyaltyFee: Este es el porcentaje de regalías que el vendedor de un NFT obtiene con cada venta.

Mappings

  • AuctionedItem: Esto contiene todos los datos del NFT acuñado en nuestra plataforma.
  • AuctionedItemExist: Usado para validar la existencia de un NFT
  • ExistingURIs: Contiene la metadata acuñada de los URIs.
  • BiddersOf: Lleva el registro de los ofertantes para una subasta en particular.

Estructuras y Eventos

  • BidderStruct: Describe la información sobre un ofertante en particular.
  • AuctionStruct: Describe la información sobre un item de un NFT en particular.
  • AuctionItemCreated: Un evento que registra la información sobre el NFT recién creado.

Funciones

  • Constructor(): Esto inicializa el contrato inteligente con la cuenta de la compañía, la tarifa de regalías estipulada y, el nombre y símbolo del token.
  • GetListingPrice(): Devuelve el precio establecido por crear un NFT en la plataforma.
  • SetListingPrice(): Usado para actualizar el precio acuñado para crear un NFT.
  • ChangePrice(): Usado para modificar el costo de un NFT en específico.
  • MintToken(): Usado para crear un nuevo token.
  • CreateAuction(): Usado para crear una nueva subasta usando el token ID acuñado.
  • OfferAuction(): Usado para subastar en la subasta.
  • ClaimPrize(): Usado para transferir el NFT a los mejores postores.
  • PerformRefund(): Usado para reembolsar los postores que no emergieron como los ganadores de cada subasta.
  • BuyAuctionItem(): Usado para comprar los NFT vendidos totalmente.
  • GetAuction(): Devuelve la subasta con el token ID.
  • GetAllAuctions(): Devuelve todas las subastas disponibles desde el contrato.
  • GetUnsoldAuction(): Devuelve todas las subastas sin vender.
  • GetSoldAuction(): Devuelve todas las subastas vendidas.
  • GetMyAuctions(): Devuelve todas las subastas que le pertenecen a la función del invocador.
  • GetLiveAuctions(): Devuelve todas las subastas listadas en el mercado.
  • GetBidders(): Devuelve a los postores de una subasta en particular, especificando en el token ID.
  • GetTimestamp(): Devuelve la marca de tiempo a una fecha en particular.
  • PayTo(): Envía ethers a una cuenta en particular.

Configurando el Script del Despliegue

Navega la carpeta de los script y luego, a tu archivo deploy.js, copia y pega el código debajo, en el mismo. Si no puedes conseguir el archivo script, crea uno, crea el archivo deploy.js y luego pega el siguiente código:

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

Cuando ejecutes el comando Hardhat, el script de arriba desplegará el contrato inteligente Auction.sol en la red local del blockchain.

Siguiendo las instrucciones de arriba, abre el terminal que apunta a este proyecto y ejecuta los comandos a continuación, separados en dos terminales. Puedes hacer esto directamente desde tu editor en VS Code.

Mira los siguientes comandos:

yarn hardhat node # Terminal 1
yarn hardhat run scripts/deploy.js # Terminal 2

Si los comandos precedentes fueron ejecutados exitosamente, deberías ver la siguiente actividad en tu terminal. Por favor, mira las imágenes:

Image description

Image description

Configurando la App Infuria

Paso 1: ve a Infuria y crea una cuenta.

Image description

Paso 2: desde el tablero principal, crea un nuevo proyecto.

Image description

Image description

Paso 3: Copia el project ID y tu API secreta en tu archivo .env en el formato siguiente y guárdalo.

Image description

Env File

INFURIA_PID=***************************
INFURIA_API=**********************
REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=******************************

Desarrollando un Procesador de Imágenes API

Necesitamos una forma de generar la metadata desde una Imagen que pretendemos hacer en NFT. El problema está en que JavaScript en el navegador, no puede darnos el resultado que queremos. Necesitaremos el script NodeJs para ayudarnos a procesar las imágenes, generar metadata, desplegar al IPFS y regresar el token URI como respuesta de la API. No hay necesidad de explicar tanto, déjame mostrarte cómo implementar esta API.

Primero, necesitarás las siguientes bibliotecas, las cuales, ya están instaladas en este proyecto, cortesía del comando de instalación yarn, que ya hemos ejecutado.

  • Express(): Autoriza la creación del servidor y compartir los recursos.
  • Express-Fileupload(): Autoriza subir archivos, por ejemplo, subir una imagen.
  • Cors(): Autoriza compartir solicitudes de origen cruzado
  • Fs(): Autoriza acceder al sistema de archivos de nuestra máquina local
  • Dotenv(): Autoriza la administración de las variables del entorno.
  • Sharp (): Autoriza el procesamiento de imágenes a diferentes dimensiones y extensiones.
  • Faker(): Autoriza la generación de data azarosa y falsa.
  • IpfsClient(): Autoriza subir los archivos al IPFS.

Ahora, vamos a escribir algunas funciones esenciales del script que, nos asistirán en convertir imágenes así como otra información como sus nombres, descripciones, precios, Ids, etc, a su equivalente en metadata.

Crea una carpeta llamada api en el directorio principal de tu proyecto, luego crea un nuevo archivo llamado metadata.js dentro del mismo, y copia y pega el código debajo:

Archivo Metadata.js

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

Ahora, vamos a utilizar estas funciones en el archivo principal de abajo, NodeJs

Archivo App.js

Crea otro script llamado app.js dentro de la misma carpeta de este API y, copia y pega los códigos de abajo; aquí es donde residirá la lógica del control de las 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

La librería IPFS usa el Portal Infuria para subir los archivos de la IPFS, en el cual, ya hemos establecido el archivo .env.

Ahora, ejecuta el nodo api/app.js en el terminal y empieza el servicio de la API, como puedes ver en la siguiente imagen.

Image description

Importando llaves Privadas a Metamask

Para usar Metamask con tu red local Hardhart, el cual es representado como Localhost:8545, usa los siguientes pasos para configurarlo.

Ejecuta yarn hardhat node en tu terminal para hacer girar el servidor local de tu blockchain. Deberías ver una imagen similar a la que está debajo, en el terminal.

Image description

Copia la llave privada de la cuenta cero (0) e impórtala en tu Metamask, como puedes ver a continuación:

Image description

Image description

Image description

Ahora, puedes repetir los pasos de arriba e importar hasta tres o cuatro cuentas, dependiendo de tus necesidades.

Todos los procesos que se necesitan para desarrollar una producción lista de contratos inteligentes, ya están almacenados en este libro, de una forma fácil de entender.

Image description

Obtén una copia de mi libro titulado “Capturando el desarrollo de contratos inteligentes, para convertirte un desarrollador de contratos inteligentes en demanda.

Desarrollando el Frontend

Ahora usaremos React para construir el front end de nuestro proyecto, usando el contrato inteligente y la información relacionada, que se ha puesto en la red y generado como artefactos (incluyendo los bytecodes y ABI). Haremos esto siguiendo los siguientes pasos:

Componentes

En el directorio src, crea una nueva carpeta llamada components para almacenar todos los componentes de React que mencionaremos:

Componente Encabezado

Ahora, crea un componente en el archivo de componentes llamado Header.jsx y pega los siguientes códigos. Los diseños de todos estos componentes fueron alcanzados usando el framework Tailwind CSS.

Image description

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

Ahora, crea otro componentes en la carpeta de componentes llamada Hero.jsx y pega los siguientes códigos de abajo:

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

Componentes Artworks

Image description

De nuevo, crea un componente en la carpeta de componentes llamada Artworks.jsx y pega los siguientes códigos de abajo

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

Componentes pie de página

Image description

Luego, crea un componente en la carpeta de componentes llamada Footer.jsx y pega los siguientes códigos de abajo:

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

Otros componentes

Los siguientes, son componentes que apoyan de forma completa, la funcionalidad del resto de esta aplicación.

Componente cuenta regresiva

Este componente es responsable de renderizar una cuenta regresiva en todos los NFT, mira el código de abajo:

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

Componente Vacío

Image description

Este componente es responsable de mostrar un pequeño texto informando a los usuarios que no hay NFT en la plataforma. Mira el código de abajo para la implementación:

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

Creando un NFT

Image description

Para escribir el componente Create NFT, usa los códigos a continuación. Esto será un modal que acepta una imagen, título, una descripción y un precio antes de enviarlo al blockchain.

Antes que sea almacenado en el blockchain, la data recogida de un formulario es enviada al api NodeJs, el cual, lo convierte en metadata y lo despliega al IPFS.

Luego, en la carpeta componentes, crea un nuevo archivo llamado CreateNFT.jsx y pega el siguiente código en el mismo:

import axios from 'axios'

import { useState } from 'react'

import { toast } from 'react-toastify'

import { FaTimes } from 'react-icons/fa'

import picture6 from '../assets/images/picture6.png'

import { setGlobalState, useGlobalState } from '../store'

import { createNftItem } from '../services/blockchain'

const CreateNFT = () => {

 const [boxModal] = useGlobalState('boxModal')

 const [name, setName] = useState('')

 const [description, setDescription] = useState('')

 const [price, setPrice] = useState('')

 const [fileUrl, setFileUrl] = useState('')

 const [imgBase64, setImgBase64] = useState(null)

 const handleSubmit = async (e) => {

   e.preventDefault()

   if (!name || !price || !description || !fileUrl) return

   const formData = new FormData()

   formData.append('name', name)

   formData.append('price', price)

   formData.append('description', description)

   formData.append('image', fileUrl)

   await toast.promise(

     new Promise(async (resolve, reject) => {

       await axios

         .post('http://localhost:9000/process', formData)

         .then(async (res) => {

           await createNftItem(res.data)

             .then(async () => {

               closeModal()

               resolve()

             })

             .catch(() => reject())

           reject()

         })

         .catch(() => reject())

     }),

     {

       pending: 'Minting & saving data to chain...',

       success: 'Minting completed, will reflect within 30sec 👌',

       error: 'Encountered error 🤯',

     },

   )

 }

 const changeImage = async (e) => {

   const reader = new FileReader()

   if (e.target.files[0]) reader.readAsDataURL(e.target.files[0])

   reader.onload = (readerEvent) => {

     const file = readerEvent.target.result

     setImgBase64(file)

     setFileUrl(e.target.files[0])

   }

 }

 const closeModal = () => {

   setGlobalState('boxModal', 'scale-0')

   resetForm()

 }

 const resetForm = () => {

   setFileUrl('')

   setImgBase64(null)

   setName('')

   setPrice('')

   setDescription('')

 }

 return (

   <div

     className={`fixed top-0 left-0 w-screen h-screen flex items-center

       justify-center bg-black bg-opacity-50 transform

       transition-transform duration-300 ${boxModal}`}

   >

     <div className="bg-[#151c25] shadow-xl shadow-[#25bd9c] rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">

       <form onSubmit={handleSubmit} className="flex flex-col">

         <div className="flex flex-row justify-between items-center">

           <p className="font-semibold text-gray-400 italic">Add NFT</p>

           <button

             type="button"

             onClick={closeModal}

             className="border-0 bg-transparent focus:outline-none"

           >

             <FaTimes className="text-gray-400" />

           </button>

         </div>

         <div className="flex flex-row justify-center items-center rounded-xl mt-5">

           <div className="shrink-0 rounded-xl overflow-hidden h-20 w-20">

             <img

               alt="NFT"

               className="h-full w-full object-cover cursor-pointer"

               src={imgBase64 || picture6}

             />

           </div>

         </div>

         <div className="flex flex-row justify-between items-center bg-gray-800 rounded-xl mt-5">

           <label className="block">

             <span className="sr-only">Choose profile photo</span>

             <input

               type="file"

               accept="image/png, image/gif, image/jpeg, image/webp"

               className="block w-full text-sm text-slate-500

                 file:mr-4 file:py-2 file:px-4

                 file:rounded-full file:border-0

                 file:text-sm file:font-semibold

                 file:bg-[#19212c] file:text-gray-300

                 hover:file:bg-[#1d2631]

                 cursor-pointer focus:ring-0 focus:outline-none"

               onChange={changeImage}

               required

             />

           </label>

         </div>

         <div className="flex flex-row justify-between items-center bg-gray-800 rounded-xl mt-5">

           <input

             className="block w-full text-sm

               text-slate-500 bg-transparent border-0

               focus:outline-none focus:ring-0 px-4 py-2"

             type="text"

             name="name"

             placeholder="Title"

             onChange={(e) => setName(e.target.value)}

             value={name}

             required

           />

         </div>

         <div className="flex flex-row justify-between items-center bg-gray-800 rounded-xl mt-5">

           <input

             className="block w-full text-sm

               text-slate-500 bg-transparent border-0

               focus:outline-none focus:ring-0 px-4 py-2"

             type="number"

             name="price"

             step={0.01}

             min={0.01}

             placeholder="Price (Eth)"

             onChange={(e) => setPrice(e.target.value)}

             value={price}

             required

           />

         </div>

         <div className="flex flex-row justify-between items-center bg-gray-800 rounded-xl mt-5">

           <textarea

             className="block w-full text-sm resize-none

               text-slate-500 bg-transparent border-0

               focus:outline-none focus:ring-0 h-18 py-2 px-4"

             type="text"

             name="description"

             placeholder="Description"

             onChange={(e) => setDescription(e.target.value)}

             value={description}

             required

           ></textarea>

         </div>

         <button

           type="submit"

           className="flex flex-row justify-center items-center

             w-full text-white text-md bg-[#25bd9c]

             py-2 px-5 rounded-full

             drop-shadow-xl border border-transparent

             hover:bg-transparent hover:text-[#ffffff]

             hover:border hover:border-[#25bd9c]

             focus:outline-none focus:ring mt-5"

         >

           Mint Now

         </button>

       </form>

     </div>

   </div>

 )

}

export default CreateNFT

Enter fullscreen mode Exit fullscreen mode

Ofreciendo NFT en el mercado

Image description

Este componente es responsable de ofrecer nuevos items en tiempo real en el mercado. Usando un formulario, acepta un tiempo de duración por el cual piensas tener tu NFT en oferta, en el mercado. Una vez que la línea de tiempo expira, el NFT desaparecerá del mercado. Mira el código de abajo:

import { useState } from 'react'

import { FaTimes } from 'react-icons/fa'

import { toast } from 'react-toastify'

import { offerItemOnMarket } from '../services/blockchain'

import { setGlobalState, useGlobalState } from '../store'

const OfferItem = () => {

 const [auction] = useGlobalState('auction')

 const [offerModal] = useGlobalState('offerModal')

 const [period, setPeriod] = useState('')

 const [biddable, setBiddable] = useState('')

 const [timeline, setTimeline] = useState('')

 const handleSubmit = async (e) => {

   e.preventDefault()

   if (!period || !biddable || !timeline) return

   const params = {

     biddable: biddable == 'true',

   }

   if (timeline == 'sec') {

     params.sec = Number(period)

     params.min = 0

     params.hour = 0

     params.day = 0

   } else if (timeline == 'min') {

     params.sec = 0

     params.min = Number(period)

     params.hour = 0

     params.day = 0

   } else if (timeline == 'hour') {

     params.sec = 0

     params.min = 0

     params.hour = Number(period)

     params.day = 0

   } else {

     params.sec = 0

     params.min = 0

     params.hour = 0

     params.day = Number(period)

   }

   await toast.promise(

     new Promise(async (resolve, reject) => {

       await offerItemOnMarket({ ...auction, ...params })

         .then(async () => {

           closeModal()

           resolve()

         })

         .catch(() => reject())

     }),

     {

       pending: 'Processing...',

       success: 'Offered on Market, will reflect within 30sec 👌',

       error: 'Encountered error 🤯',

     },

   )

 }

 const closeModal = () => {

   setGlobalState('offerModal', 'scale-0')

   setPeriod('')

   setBiddable('')

 }

 return (


   <div

     className={`fixed top-0 left-0 w-screen h-screen flex items-center

       justify-center bg-black bg-opacity-50 transform

       transition-transform timeline-300 ${offerModal}`}

   >

     <div className="bg-[#151c25] shadow-xl shadow-[#25bd9c] rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">


       <form onSubmit={handleSubmit} className="flex flex-col">

         <div className="flex flex-row justify-between items-center">

           <p className="font-semibold text-gray-400 italic">

             {auction?.name}

           </p>

           <button

             type="button"

             onClick={closeModal}

             className="border-0 bg-transparent focus:outline-none"

           >

             <FaTimes className="text-gray-400" />

           </button>

         </div>

         <div className="flex flex-row justify-center items-center rounded-xl mt-5">

           <div className="shrink-0 rounded-xl overflow-hidden h-20 w-20">

             <img

               alt="NFT"

               className="h-full w-full object-cover cursor-pointer"

               src={auction?.image}

             />

           </div>

         </div>

         <div className="flex flex-row justify-between items-center bg-gray-800 rounded-xl mt-5">

           <input

             className="block w-full text-sm

               text-slate-500 bg-transparent border-0

               focus:outline-none focus:ring-0 px-4 py-2"

             type="number"

             name="period"

             min={1}

             placeholder="Days E.g 7"

             onChange={(e) => setPeriod(e.target.value)}

             value={period}

             required

           />

         </div>

         <div className="flex flex-row justify-between items-center bg-gray-800 rounded-xl mt-5">

           <select

             className="block w-full text-sm

             text-slate-500 bg-transparent border-0

             focus:outline-none focus:ring-0 px-4 py-2"

             name="biddable"

             onChange={(e) => setTimeline(e.target.value)}

             value={timeline}

             required

           >

             <option value="" hidden>

               Select Duration

             </option>

             <option value="sec">Seconds</option>

             <option value="min">Minutes</option>

             <option value="hour">Hours</option>

             <option value="day">Days</option>

           </select>

         </div>

         <div className="flex flex-row justify-between items-center bg-gray-800 rounded-xl mt-5">

           <select

             className="block w-full text-sm

             text-slate-500 bg-transparent border-0

             focus:outline-none focus:ring-0 px-4 py-2"

             name="biddable"

             onChange={(e) => setBiddable(e.target.value)}

             value={biddable}

             required

           >

             <option value="" hidden>

               Select Biddability

             </option>

             <option value={true}>Yes</option>

             <option value={false}>No</option>

           </select>

         </div>

         <button

           type="submit"

           className="flex flex-row justify-center items-center

             w-full text-white text-md bg-[#25bd9c]

             py-2 px-5 rounded-full

             drop-shadow-xl border border-transparent

             hover:bg-transparent hover:text-[#ffffff]

             hover:border hover:border-[#25bd9c]

             focus:outline-none focus:ring mt-5"

         >

           Offer Item

         </button>

       </form>

     </div>

   </div>

 )

}

export default OfferItem

Enter fullscreen mode Exit fullscreen mode

Colocando Ofertas

Image description

Este componente permite a los usuarios colocar ofertas y participar en una subasta de NFT. Esto se logra a través del uso de un modal que recibe el precio que el usuario pretende ofertar, si está dentro del tiempo límite para ofertar. Mira el listado de códigos de abajo:

import { useState } from 'react'

import { FaTimes } from 'react-icons/fa'

import { toast } from 'react-toastify'

import { bidOnNFT } from '../services/blockchain'

import { setGlobalState, useGlobalState } from '../store'

const PlaceBid = () => {

 const [auction] = useGlobalState('auction')

 const [bidBox] = useGlobalState('bidBox')

 const [price, setPrice] = useState('')

 const closeModal = () => {

   setGlobalState('bidBox', 'scale-0')

   setPrice('')

 }

 const handleBidPlacement = async (e) => {

   e.preventDefault()

   if (!price) return

   await toast.promise(

     new Promise(async (resolve, reject) => {

       await bidOnNFT({ ...auction, price })

         .then(() => {

           resolve()

           closeModal()

         })

         .catch(() => reject())

     }),

     {
       pending: 'Processing...',

       success: 'Bid placed successful, will reflect within 30sec 👌',

       error: 'Encountered error 🤯',

     },

   )

 }

 return (

   <div

     className={`fixed top-0 left-0 w-screen h-screen flex items-center

       justify-center bg-black bg-opacity-50 transform

       transition-transform duration-300 ${bidBox}`}

   >

     <div className="bg-[#151c25] shadow-xl shadow-[#25bd9c] rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">

       <form onSubmit={handleBidPlacement} className="flex flex-col">

         <div className="flex flex-row justify-between items-center">

           <p className="font-semibold text-gray-400 italic">

             {auction?.name}

           </p>

           <button

             type="button"

             onClick={closeModal}

             className="border-0 bg-transparent focus:outline-none"

           >

             <FaTimes className="text-gray-400" />

           </button>

         </div>

         <div className="flex flex-row justify-center items-center rounded-xl mt-5">

           <div className="shrink-0 rounded-xl overflow-hidden h-20 w-20">

             <img

               alt="NFT"

               className="h-full w-full object-cover cursor-pointer"

               src={auction?.image}

             />

           </div>

         </div>

         <div className="flex flex-row justify-between items-center bg-gray-800 rounded-xl mt-5">

           <input

             className="block w-full text-sm

               text-slate-500 bg-transparent border-0

               focus:outline-none focus:ring-0 px-4 py-2"

             type="number"

             name="price"

             step={0.01}

             min={0.01}

             placeholder="Price (Eth)"

             onChange={(e) => setPrice(e.target.value)}

             value={price}

             required

           />

         </div>

         <button

           type="submit"

           className="flex flex-row justify-center items-center

             w-full text-white text-md bg-[#25bd9c]

             py-2 px-5 rounded-full

             drop-shadow-xl border border-transparent

             hover:bg-transparent hover:text-[#ffffff]

             hover:border hover:border-[#25bd9c]

             focus:outline-none focus:ring mt-5"

         >

           Place Bid

         </button>

       </form>

     </div>

   </div>

 )

}

export default PlaceBid

Enter fullscreen mode Exit fullscreen mode

Cambiando el precio de un NFT

Image description

Este componente permite al dueño de un NFT cambiar el precio de un NFT que no se está ofertando en el mercado. Se logra aceptar este nuevo precio desde un formulario y luego enviarlo a la blockchain. Mira los códigos de abajo:

import { useState } from 'react'

import { toast } from 'react-toastify'

import { FaTimes } from 'react-icons/fa'

import { updatePrice } from '../services/blockchain'

import { setGlobalState, useGlobalState } from '../store'

const ChangePrice = () => {

 const [auction] = useGlobalState('auction')

 const [priceModal] = useGlobalState('priceModal')

 const [price, setPrice] = useState('')

 const handleSubmit = async (e) => {

   e.preventDefault()

   if (!price) return

   await toast.promise(

     new Promise(async (resolve, reject) => {

       await updatePrice({ ...auction, price })

         .then(async () => {

           closeModal()

           resolve()

         })

         .catch(() => reject())

     }),

     {

       pending: 'Processing...',

       success: 'Price updated, will reflect within 30sec 👌',

       error: 'Encountered error 🤯',

     },

   )

 }

 const closeModal = () => {

   setGlobalState('priceModal', 'scale-0')

   setPrice('')

 }

 return (

   <div

     className={`fixed top-0 left-0 w-screen h-screen flex items-center

       justify-center bg-black bg-opacity-50 transform

       transition-transform timeline-300 ${priceModal}`}

   >

     <div className="bg-[#151c25] shadow-xl shadow-[#25bd9c] rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">

       <form onSubmit={handleSubmit} className="flex flex-col">

         <div className="flex flex-row justify-between items-center">

           <p className="font-semibold text-gray-400 italic">

             {auction?.name}

           </p>

           <button

             type="button"

             onClick={closeModal}

             className="border-0 bg-transparent focus:outline-none"

           >

             <FaTimes className="text-gray-400" />

           </button>

         </div>

         <div className="flex flex-row justify-center items-center rounded-xl mt-5">

           <div className="shrink-0 rounded-xl overflow-hidden h-20 w-20">
             <img

               alt="NFT"

               className="h-full w-full object-cover cursor-pointer"

               src={auction?.image}

             />

           </div>

         </div>

         <div className="flex flex-row justify-between items-center bg-gray-800 rounded-xl mt-5">

           <input

             className="block w-full text-sm

               text-slate-500 bg-transparent border-0

               focus:outline-none focus:ring-0 px-4 py-2"

             type="number"

             name="price"

             step={0.01}

             min={0.01}

             placeholder="Days E.g 2.3 ETH"

             onChange={(e) => setPrice(e.target.value)}

             value={price}

             required

           />

         </div>

         <button

           type="submit"

           className="flex flex-row justify-center items-center

             w-full text-white text-md bg-[#25bd9c]

             py-2 px-5 rounded-full

             drop-shadow-xl border border-transparent

             hover:bg-transparent hover:text-[#ffffff]

             hover:border hover:border-[#25bd9c]

             focus:outline-none focus:ring mt-5"

         >

           Change Price

         </button>

       </form>

     </div>

   </div>

 )

}

export default ChangePrice

Enter fullscreen mode Exit fullscreen mode

El componente del Chat

Image description

Finalmente, de los componentes, está el componente del chat que es controlado por el CometChat SDK. Mira los códigos de abajo:

import { useEffect, useState } from 'react'

import Identicon from 'react-identicons'

import { toast } from 'react-toastify'

import { getMessages, listenForMessage, sendMessage } from '../services/chat'

import { truncate, useGlobalState } from '../store'

const Chat = ({ id, group }) => {

 const [message, setMessage] = useState('')

 const [messages, setMessages] = useState([])

 const [connectedAccount] = useGlobalState('connectedAccount')

 const handleSubmit = async (e) => {

   e.preventDefault()

   if (!message) return

   await sendMessage(`pid_${id}`, message)

     .then(async (msg) => {

       setMessages((prevState) => [...prevState, msg])

       setMessage('')

       scrollToEnd()

     })

     .catch((error) => {

       toast.error('Encountered Error, check the console')

       console.log(error)

     })

 }

 useEffect(async () => {

   await getMessages(`pid_${id}`)

     .then((msgs) => {

       setMessages(msgs)

       scrollToEnd()

     })

     .catch((error) => console.log(error))

   await listenForMessage(`pid_${id}`)

     .then((msg) => {

       setMessages((prevState) => [...prevState, msg])

       scrollToEnd()

     })

     .catch((error) => console.log(error))

 }, [])

 const scrollToEnd = () => {

   const elmnt = document.getElementById('messages-container')

   elmnt.scrollTop = elmnt.scrollHeight

 }

 return (

   <div>

     <h2 className="mt-12 px-2 py-1 font-bold text-2xl italic">NFT-World</h2>

     <h4 className="px-2 font-semibold text-xs">Join the Live Chat</h4>

     <div

       className="bg-gray-800 bg-opacity-50 w-full

       rounded-md p-2 sm:p-8 mt-5 shadow-md shadow-[#25bd9c]"

     >

       <div

         id="messages-container"

         className="h-[calc(100vh_-_30rem)] overflow-y-auto"

       >

         {messages.map((msg, i) => (

           <Message

             isOwner={msg.sender.uid == connectedAccount}

             owner={msg.sender.uid}

             msg={msg.text}

             key={i}

           />

         ))}

       </div>

       <form

         onSubmit={handleSubmit}

         className="flex flex-row justify-between items-center bg-gray-800 rounded-md"

       >

         <input

           className="block w-full text-sm resize-none

           text-slate-100 bg-transparent border-0

             focus:outline-none focus:ring-0 h-15 px-4 py-4"

           type="text"

           name="Leave a Message"

           placeholder={

             !group?.hasJoined

               ? 'Join group first to chat...'

               : 'Leave a Message...'

           }

           disabled={!group?.hasJoined}

           value={message}

           onChange={(e) => setMessage(e.target.value)}

           required

         />

         <button type="submit" hidden>

           Send

         </button>

       </form>

     </div>

   </div>

 )

}

const Message = ({ msg, owner, isOwner }) => (

 <div>

   <div className="flex justify-start items-center space-x-1 mb-2">

     <Identicon

       string={owner}

       className="h-5 w-5 object-contain bg-gray-800 rounded-full"

       size={18}

     />

     <div className="space-x-1">

       <span className="text-[#25bd9c] font-bold">

         {isOwner ? '@You' : truncate(owner, 4, 4, 11)}

       </span>

       <span className="text-gray-200 text-xs">{msg}</span>

     </div>

   </div>

 </div>

)

export default Chat

Enter fullscreen mode Exit fullscreen mode

Las Páginas

Esta aplicación tiene alrededor de tres vistas o páginas; vamos a organizar todos los componentes de arriba en sus respectivas vistas usando los pasos de abajo. Primero, crearemos una carpeta llamada views en el directorio src y crearemos las páginas que discutiremos pronto.

Vista Inicial

Image description

La vista inicial combina dos componentes mayores: los componentes Hero y Artworks. Mira los códigos de abajo:

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

Vista de Colecciones

Image description

Esta vista dispone todos los NFT que son de un dueño en particular. Permite al usuario administrar un NFT como por ejemplo, ofrecerlo (o no) en el mercado o cambiar su precio. Mira los códigos de abajo:

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

La vista NFT

Image description

Por último, esta vista contiene los componentes del chat, así como otros componentes importantes, como se muestra en el código de abajo:

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

Actualizando el archivo App.jsx

Actualiza el archivo de la app con los códigos de abajo para poder juntar todos los componentes y 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

Actualizar el Index.jsx y los archivos CSS

Usa los códigos de abajo para actualizar los archivos index.jsx y el 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;

view rawIndex.css hosted with ❤ by GitHub

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

Añadiendo los servicios de la App

En esta aplicación, tendremos dos servicios: chat y blockchain, como se muestra en los códigos a continuación. Simplemente, crea una nueva carpeta llamada services en el directorio src y coloca los siguientes archivos, usando los códigos de abajo:

Servicios del Chat

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

Servicio del Blockchain

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

La tienda

La tienda es un servicio de estado de administración, incluída en esta aplicación. Aquí es donde toda la data extraída desde el blockchain se mantiene. Para replicarlo, crea una carpeta de almacenamiento dentro del directorio src. Luego, dentro de esta carpeta, crea un archivo llamado index.jsx y pega los códigos de abajo:

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

Ahora comienza la aplicación ejecutando yarn start en otro terminal para ver el resultado en el terminal. Si encuentras cualquier problema replicando este proyecto, puedes consultarnos en nuestro canal de discord.

También puedes ver este video para aprender más sobre cómo construir un mercado de NFT, desde el diseño hasta el despliegue.

Felicidades, así es cómo construyes un mercado de NFT usando: Reacto, Solidity y CometChat.

Conclusión

En conclusión, construir un sitio de subastas de NFT con React, Solidity y CometChat requiere una combinación de desarrollo front end y back end.

Usando estas herramientas juntas, es posible crear una casa de subastas de NFT, totalmente funcional que es segura, escalable y amigable con el usuario.

Si estás listo para ir más profundo sobre el desarrollo web3, programa unas clases de web3 conmigo en privado, para acelerar tu aprendizaje web3.

Dicho esto, te veo en la próxima y ¡que tengas un muy buen día!

Sobre el autor

Gospel Darlington es un desarrollador full-stack de blockchain con más de 6 años de experiencia en la industria de desarrollo de software.

Combinando el desarrollo de software, escritura y enseñanza, el demuestra cómo construir aplicaciones descentralizadas en redes de blockchain compatibles con EVM.

Sus stack incluyen: JavaScript, React, Vue, Angular, Node, React Native, NextJs, Solidity y más.

Para más información sobre él, visita y sigue su página de Twitter, GitHub, LinkedIn o su página web

Discussion (0)