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"
]
}
}
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
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:
Paso 2:
Inicia sesión en el tablero principal de CometChat, luego de registrarte.
Paso 3:
Desde el tablero principal, añade una nueva aplicación llamada Auction.
Paso 4:
Selecciona la app que has creado, desde la lista:
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
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
}
}
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);
}
}
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
})
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:
Configurando la App Infuria
Paso 1: ve a Infuria y crea una cuenta.
Paso 2: desde el tablero principal, crea un nuevo proyecto.
Paso 3: Copia el project ID y tu API secreta en tu archivo .env
en el formato siguiente y guárdalo.
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
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...')
})
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.
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.
Copia la llave privada de la cuenta cero (0) e impórtala en tu Metamask, como puedes ver a continuación:
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.
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.
import { Link } from 'react-router-dom'
import { connectWallet } from '../services/blockchain'
import { truncate, useGlobalState } from '../store'
const Header = () => {
const [connectedAccount] = useGlobalState('connectedAccount')
return (
<nav className="w-4/5 flex flex-row md:justify-center justify-between items-center py-4 mx-auto">
<div className="md:flex-[0.5] flex-initial justify-center items-center">
<Link to="/" className="text-white">
<span className="px-2 py-1 font-bold text-3xl italic">Dapp</span>
<span className="py-1 font-semibold italic">Auction-NFT</span>
</Link>
</div>
<ul
className="md:flex-[0.5] text-white md:flex
hidden list-none flex-row justify-between
items-center flex-initial"
>
<Link to="/" className="mx-4 cursor-pointer">Market</Link>
<Link to="/collections" className="mx-4 cursor-pointer">Collection</Link>
<Link className="mx-4 cursor-pointer">Artists</Link>
<Link className="mx-4 cursor-pointer">Community</Link>
</ul>
{connectedAccount ? (
<button
className="shadow-xl shadow-black text-white
bg-green-500 hover:bg-green-700 md:text-xs p-2
rounded-full cursor-pointer text-xs sm:text-base"
>
{truncate(connectedAccount, 4, 4, 11)}
</button>
) : (
<button
className="shadow-xl shadow-black text-white
bg-green-500 hover:bg-green-700 md:text-xs p-2
rounded-full cursor-pointer text-xs sm:text-base"
onClick={connectWallet}
>
Connect Wallet
</button>
)}
</nav>
)
}
export default Header
Componente Hero
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
Componentes Artworks
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
Componentes pie de página
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">
©2022 All rights reserved
</p>
</div>
)
}
export default Footer
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
Componente Vacío
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
Creando un NFT
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
Ofreciendo NFT en el mercado
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
Colocando Ofertas
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
Cambiando el precio de un NFT
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
El componente del Chat
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
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
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
Vista de Colecciones
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
La vista NFT
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
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
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'),
)
})
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,
}
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,
}
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,
}
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)