Construyendo un Marketplace de NFT de Pila Completa en Ethereum con Polygon

Lorena Fabris
22 min readFeb 12, 2022

--

Por Nader Dabit, 6 de julio de 2021, actualizado el 9 de febrero de 2022. Traducción del artículo “Building a Full Stack NFT Marketplace on Ethereum” por Lorena Fabris

#webdev #react #blockchain #ethereum

Construyendo un marketplace digital con Polygon, Next.js, Tailwind, Solidity, Hardhat, Ethers.js, y IPFS

  • Para ver el curso de video de este tutorial, haz clic aquí

En mi último tutorial de Ethereum de principio a fin, The Complete Guide to Full Stack Ethereum Development, introduje cómo construir una aplicación básica en Ethereum utilizando herramientas modernas como Hardhat y Ethers.js.

En esta guía aprenderás a construir, desplegar y probar un marketplace de NFT de pila completa en Ethereum. También veremos cómo desplegar en Polygon.

Una cosa que se ha hecho evidente en los últimos meses es la rapidez con la que las soluciones de escalado de Ethereum como Polygon, Arbitrum,y Optimism están ganando impulso y adopción. Estas tecnologías permiten a los desarrolladores construir las mismas aplicaciones que harían directamente en Ethereum con los beneficios añadidos de menores costos de gas y mayor velocidad de transacción, entre otras cosas.

Debido a la propuesta de valor que ofrecen estas soluciones, combinada con la falta general de contenido existente, voy a construir varios proyectos de ejemplo y tutoriales para aplicaciones de pila completa utilizando estas diversas soluciones de escalado de Ethereum, comenzando con este en Polygon.

Requisitos previos

Para tener éxito en esta guía, debes tener lo siguiente:

  1. Node.js instalado en tu máquina
  2. La extensión de wallet Metamask instalada como extensión del navegador

La pila

En esta guía, vamos a construir una aplicación de pila completa utilizando:

Marco de aplicación web Next.js

Entorno de desarrollo SolidityHardhat

Almacenamiento de archivos IPFS

Biblioteca de cliente web de Ethereum Ethers.js

Aunque no formará parte de esta guía (se publicará en un post aparte), veremos cómo construir una capa de API más robusta utilizando The Graph Protocol para sortear las limitaciones en los patrones de acceso a datos proporcionados por la capa nativa de blockchain.

Sobre el proyecto

El proyecto que construiremos será Metaverse Marketplace, un marketplace digital.

Cuando un usuario pone un artículo a la venta, la propiedad del artículo se transfiere del creador al marketplace.

Cuando un usuario compre un artículo, el precio de compra se transferirá del comprador al vendedor y el artículo se transferirá del marketplace al comprador.

El propietario del marketplace podrá establecer una tarifa (fee) de publicación. Esta tarifa se cobrará al vendedor y se transferirá al propietario del contrato al finalizar cualquier venta, lo que permitirá al propietario del marketplace obtener ingresos recurrentes de cualquier venta realizada en el marketplace.

La lógica del marketplace consistirá en dos contratos inteligentes:

Contrato NFT — Este contrato permite a los usuarios acuñar (mintear) activos digitales únicos.

Contrato de Marketplace — Este contrato permite a los usuarios poner sus activos digitales a la venta en un mercado abierto.

Creo que se trata de un buen proyecto porque las herramientas, técnicas e ideas con las que trabajaremos sientan las bases para muchos otros tipos de aplicaciones en esta pila, que se ocupan de cosas como los pagos, las comisiones y las transferencias de propiedad a nivel de contrato, así como de la forma en que una aplicación del lado del cliente utilizaría este contrato inteligente para construir una interfaz de usuario eficiente y de aspecto agradable.

Además del contrato inteligente, también te mostraré cómo construir un subgrafo para que la consulta de datos del contrato inteligente sea más flexible y eficiente. Como verás, crear vistas sobre conjuntos de datos y habilitar patrones de acceso a los datos variados y de alto rendimiento es difícil de hacer directamente desde un contrato inteligente. The Graph hace esto mucho más fácil.

Acerca de Polygon

De los documentos:

“Polygon es un protocolo y un marco para construir y conectar redes de blockchain compatibles con Ethereum. Agrega soluciones escalables en Ethereum apoyando un ecosistema Ethereum multichain”.

Polygon es unas 10 veces más rápido que Ethereum y sin embargo las transacciones son más de 10 veces más baratas.

Bien, pero ¿Qué significa todo esto?

Para mí significa que puedo utilizar los mismos conocimientos, herramientas y tecnologías que he estado utilizando para construir aplicaciones en Ethereum para construir aplicaciones que son más rápidas y más baratas para los usuarios, proporcionando no sólo una mejor experiencia de usuario, sino también abriendo la puerta a muchos tipos de aplicaciones que simplemente no serían factibles de ser construidas directamente en Ethereum.

Como mencioné anteriormente, hay muchas otras soluciones de escalado de Ethereum, como Arbitrum y Optimism, que también se encuentran en un espacio similar. La mayoría de estas soluciones de escalado tienen diferencias técnicas y se dividen en varias categorías como sidechains , layer 2s, y state channels.

Polygon ha cambiado recientemente de marca desde Matic, por lo que también verás la palabra Matic usada indistintamente cuando se refiera a varias partes de su ecosistema, ya que el nombre todavía se utiliza en varios lugares, como sus nombres de tokens y redes.

Para obtener más información sobre Polygon, consulta este artículo y su documentación aquí.

Ahora que tenemos una visión general del proyecto y de las tecnologías relacionadas, ¡empecemos a construir!

Configuración del proyecto

Para empezar, crearemos una nueva aplicación Next.js. Para ello, abre tu terminal. Crea o cambia a un nuevo directorio vacío y ejecuta el siguiente comando:

npx create-next-app digital-marketplace

A continuación, cambia al nuevo directorio e instala las dependencias:

cd digital-marketplacenpm install ethers hardhat @nomiclabs/hardhat-waffle \
ethereum-waffle chai @nomiclabs/hardhat-ethers \
web3modal @openzeppelin/contracts ipfs-http-client@50.1.2 \
axios

Configuración de Tailwind CSS

Vamos a utilizar Tailwind CSS para el estilo, vamos a configurarlo en este paso.

Tailwind es un marco de trabajo CSS que facilita la adición de estilos y la creación de sitios web de buen aspecto sin mucho trabajo.

A continuación, instala las dependencias de Tailwind:

npm install -D tailwindcss@latest postcss@latest autoprefixer@latest

A continuación, crearemos los archivos de configuración necesarios para que Tailwind funcione con Next.js (tailwind.config.js y postcss.config.js) ejecutando el siguiente comando:

npx tailwindcss init -p

A continuación, configura las rutas de contentde tu plantilla en tailwind.config.js:

/* tailwind.config.js */
module.exports = {
content: [
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

Por último, elimina el código en styles/globals.css y actualízalo con lo siguiente:

@tailwind base;
@tailwind components;
@tailwind utilities;

Configuración de Hardhat

A continuación, inicializa un nuevo entorno de desarrollo Hardhat desde la raíz de tu proyecto:

npx hardhat

? What do you want to do? Create a sample project
? Hardhat project root: <Choose default path>

Ahora deberías ver los siguientes archivos y carpetas creados para ti en tu directorio raíz:

hardhat.config.js — La totalidad de tu configuración de Hardhat (es decir, tu configuración, plugins y tareas personalizadas) está contenida en este archivo.

scripts — Una carpeta que contiene un script llamado sample-script.js que desplegará tu contrato inteligente cuando se ejecute

test — Una carpeta que contiene un script de prueba de ejemplo

contracts — Una carpeta que contiene un ejemplo de contrato inteligente Solidity

A continuación, actualiza la configuración en hardhat.config.js con lo siguiente:

/* hardhat.config.js */
require("@nomiclabs/hardhat-waffle")
const fs = require('fs')
const privateKey = fs.readFileSync(".secret").toString().trim() || "01234567890123456789"

module.exports = {
defaultNetwork: "hardhat",
networks: {
hardhat: {
chainId: 1337
},
// mumbai: {
// url: "https://rpc-mumbai.maticvigil.com",
// accounts: [privateKey]
// }
},
solidity: {
version: "0.8.4",
settings: {
optimizer: {
enabled: true,
runs: 200
}
}
}
}

En esta configuración, hemos configurado el entorno de desarrollo local de Hardhat, así como la red de pruebas de Mumbai.

Puedes leer más sobre ambas redes Matic aquí.

A continuación, crea un archivo llamado .secret en la raíz de tu proyecto. Por ahora, dejaremos este archivo vacío. Más adelante, lo rellenaremos con una clave privada de la wallet de prueba que contendrá algunos tokens de Matic que obtendremos de la red de prueba de Matic.

  • Asegúrate de no confirmar nunca las claves privadas en Git. Para estar más seguro, considera almacenar estos valores en variables de entorno temporales cuando trabajes con wallets que contengan tokens reales. Para omitirlas de Git, añade .secret a tu archivo .gitignore.

Contratos inteligentes

A continuación, ¡crearemos nuestros contratos inteligentes! Empezaremos con el contrato NFT para los activos digitales únicos.

Crea un nuevo archivo en el directorio de contratos llamado NFT.sol. Aquí, añade el siguiente código:

// contracts/NFT.sol
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.3;

import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

import "hardhat/console.sol";

contract NFT is ERC721URIStorage {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
address contractAddress;

constructor(address marketplaceAddress) ERC721("Metaverse Tokens", "METT") {
contractAddress = marketplaceAddress;
}

function createToken(string memory tokenURI) public returns (uint) {
_tokenIds.increment();
uint256 newItemId = _tokenIds.current();

_mint(msg.sender, newItemId);
_setTokenURI(newItemId, tokenURI);
setApprovalForAll(contractAddress, true);
return newItemId;
}
}

Este es un contrato inteligente NFT bastante sencillo que permite a los usuarios acuñar (mintear) activos digitales únicos y tener la propiedad de los mismos.

En este contrato estamos heredando del estándar ERC721 implementado por OpenZeppelin

A continuación, crearemos el contrato para el Marketplace. Este es un contrato inteligente mucho más grande. He hecho todo lo posible para documentar lo que hace cada función.

Crea un nuevo archivo en el directorio de contratos llamado Market.sol:

// contracts/Market.sol
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.3;

import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

import "hardhat/console.sol";

contract NFTMarket is ReentrancyGuard {
using Counters for Counters.Counter;
Counters.Counter private _itemIds;
Counters.Counter private _itemsSold;

address payable owner;
uint256 listingPrice = 0.025 ether;

constructor() {
owner = payable(msg.sender);
}

struct MarketItem {
uint itemId;
address nftContract;
uint256 tokenId;
address payable seller;
address payable owner;
uint256 price;
bool sold;
}

mapping(uint256 => MarketItem) private idToMarketItem;

event MarketItemCreated (
uint indexed itemId,
address indexed nftContract,
uint256 indexed tokenId,
address seller,
address owner,
uint256 price,
bool sold
);

/* Returns the listing price of the contract */
function getListingPrice() public view returns (uint256) {
return listingPrice;
}

/* Places an item for sale on the marketplace */
function createMarketItem(
address nftContract,
uint256 tokenId,
uint256 price
) public payable nonReentrant {
require(price > 0, "Price must be at least 1 wei");
require(msg.value == listingPrice, "Price must be equal to listing price");

_itemIds.increment();
uint256 itemId = _itemIds.current();

idToMarketItem[itemId] = MarketItem(
itemId,
nftContract,
tokenId,
payable(msg.sender),
payable(address(0)),
price,
false
);

IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId);

emit MarketItemCreated(
itemId,
nftContract,
tokenId,
msg.sender,
address(0),
price,
false
);
}

/* Creates the sale of a marketplace item */
/* Transfers ownership of the item, as well as funds between parties */
function createMarketSale(
address nftContract,
uint256 itemId
) public payable nonReentrant {
uint price = idToMarketItem[itemId].price;
uint tokenId = idToMarketItem[itemId].tokenId;
require(msg.value == price, "Please submit the asking price in order to complete the purchase");

idToMarketItem[itemId].seller.transfer(msg.value);
IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId);
idToMarketItem[itemId].owner = payable(msg.sender);
idToMarketItem[itemId].sold = true;
_itemsSold.increment();
payable(owner).transfer(listingPrice);
}

/* Returns all unsold market items */
function fetchMarketItems() public view returns (MarketItem[] memory) {
uint itemCount = _itemIds.current();
uint unsoldItemCount = _itemIds.current() - _itemsSold.current();
uint currentIndex = 0;

MarketItem[] memory items = new MarketItem[](unsoldItemCount);
for (uint i = 0; i < itemCount; i++) {
if (idToMarketItem[i + 1].owner == address(0)) {
uint currentId = i + 1;
MarketItem storage currentItem = idToMarketItem[currentId];
items[currentIndex] = currentItem;
currentIndex += 1;
}
}
return items;
}

/* Returns only items that a user has purchased */
function fetchMyNFTs() public view returns (MarketItem[] memory) {
uint totalItemCount = _itemIds.current();
uint itemCount = 0;
uint currentIndex = 0;

for (uint i = 0; i < totalItemCount; i++) {
if (idToMarketItem[i + 1].owner == msg.sender) {
itemCount += 1;
}
}

MarketItem[] memory items = new MarketItem[](itemCount);
for (uint i = 0; i < totalItemCount; i++) {
if (idToMarketItem[i + 1].owner == msg.sender) {
uint currentId = i + 1;
MarketItem storage currentItem = idToMarketItem[currentId];
items[currentIndex] = currentItem;
currentIndex += 1;
}
}
return items;
}

/* Returns only items a user has created */
function fetchItemsCreated() public view returns (MarketItem[] memory) {
uint totalItemCount = _itemIds.current();
uint itemCount = 0;
uint currentIndex = 0;

for (uint i = 0; i < totalItemCount; i++) {
if (idToMarketItem[i + 1].seller == msg.sender) {
itemCount += 1;
}
}

MarketItem[] memory items = new MarketItem[](itemCount);
for (uint i = 0; i < totalItemCount; i++) {
if (idToMarketItem[i + 1].seller == msg.sender) {
uint currentId = i + 1;
MarketItem storage currentItem = idToMarketItem[currentId];
items[currentIndex] = currentItem;
currentIndex += 1;
}
}
return items;
}
}

Ahora el código y el entorno del contrato inteligente están completos y podemos probarlo.

Para ello, podemos crear una prueba local para ejecutar gran parte de la funcionalidad, como acuñar un token, ponerlo a la venta, venderlo a un usuario, y consultar por tokens.

Para crear la prueba, abre test/sample-test.js y actualízalo con el siguiente código:

/* test/sample-test.js */
describe("NFTMarket", function() {
it("Should create and execute market sales", async function() {
/* deploy the marketplace */
const Market = await ethers.getContractFactory("NFTMarket")
const market = await Market.deploy()
await market.deployed()
const marketAddress = market.address

/* deploy the NFT contract */
const NFT = await ethers.getContractFactory("NFT")
const nft = await NFT.deploy(marketAddress)
await nft.deployed()
const nftContractAddress = nft.address

let listingPrice = await market.getListingPrice()
listingPrice = listingPrice.toString()

const auctionPrice = ethers.utils.parseUnits('1', 'ether')

/* create two tokens */
await nft.createToken("https://www.mytokenlocation.com")
await nft.createToken("https://www.mytokenlocation2.com")

/* put both tokens for sale */
await market.createMarketItem(nftContractAddress, 1, auctionPrice, { value: listingPrice })
await market.createMarketItem(nftContractAddress, 2, auctionPrice, { value: listingPrice })

const [_, buyerAddress] = await ethers.getSigners()

/* execute sale of token to another user */
await market.connect(buyerAddress).createMarketSale(nftContractAddress, 1, { value: auctionPrice})

/* query for and return the unsold items */
items = await market.fetchMarketItems()
items = await Promise.all(items.map(async i => {
const tokenUri = await nft.tokenURI(i.tokenId)
let item = {
price: i.price.toString(),
tokenId: i.tokenId.toString(),
seller: i.seller,
owner: i.owner,
tokenUri
}
return item
}))
console.log('items: ', items)
})
})

Para ejecutar la prueba, ejecuta npx hardhat test desde la línea de tus comandos:

* Si la prueba se ejecuta con éxito, debería registrar un array que contenga un único elemento del marketplace.

Construir el front end

Ahora que el contrato inteligente funciona y está listo para funcionar, podemos empezar a construir la interfaz de usuario.

Lo primero que debemos pensar es en configurar un diseño para que podamos habilitar alguna navegación que persista en todas las páginas.

Para configurar esto, abre pages/_app.js y actualízalo con el siguiente código:

/* pages/_app.js */
import '../styles/globals.css'
import Link from 'next/link'

function MyApp({ Component, pageProps }) {
return (
<div>
<nav className="border-b p-6">
<p className="text-4xl font-bold">Metaverse Marketplace</p>
<div className="flex mt-4">
<Link href="/">
<a className="mr-4 text-pink-500">
Home
</a>
</Link>
<Link href="/create-item">
<a className="mr-6 text-pink-500">
Sell Digital Asset
</a>
</Link>
<Link href="/my-assets">
<a className="mr-6 text-pink-500">
My Digital Assets
</a>
</Link>
<Link href="/creator-dashboard">
<a className="mr-6 text-pink-500">
Creator Dashboard
</a>
</Link>
</div>
</nav>
<Component {...pageProps} />
</div>
)
}

export default MyApp

La navegación tiene enlaces para la ruta de inicio, así como una página para vender un activo digital, ver los activos que has comprado, y un panel de creador para ver los activos que has creado, así como los activos que has vendido.

Consulta del contrato de los artículos del marketplace

La siguiente página que actualizaremos es pages/index.js. Este es el punto de entrada principal de la aplicación, y será la vista donde consultamos los activos digitales para la venta y los mostramos en la pantalla.

/* pages/index.js */
import { ethers } from 'ethers'
import { useEffect, useState } from 'react'
import axios from 'axios'
import Web3Modal from "web3modal"
import {
nftaddress, nftmarketaddress
} from '../config'
import NFT from '../artifacts/contracts/NFT.sol/NFT.json'
import Market from '../artifacts/contracts/Market.sol/NFTMarket.json'
export default function Home() {
const [nfts, setNfts] = useState([])
const [loadingState, setLoadingState] = useState('not-loaded')
useEffect(() => {
loadNFTs()
}, [])
async function loadNFTs() {
/* create a generic provider and query for unsold market items */
const provider = new ethers.providers.JsonRpcProvider()
const tokenContract = new ethers.Contract(nftaddress, NFT.abi, provider)
const marketContract = new ethers.Contract(nftmarketaddress, Market.abi, provider)
const data = await marketContract.fetchMarketItems()
/*
* map over items returned from smart contract and format
* them as well as fetch their token metadata
*/
const items = await Promise.all(data.map(async i => {
const tokenUri = await tokenContract.tokenURI(i.tokenId)
const meta = await axios.get(tokenUri)
let price = ethers.utils.formatUnits(i.price.toString(), 'ether')
let item = {
price,
tokenId: i.tokenId.toNumber(),
seller: i.seller,
owner: i.owner,
image: meta.data.image,
name: meta.data.name,
description: meta.data.description,
}
return item
}))
setNfts(items)
setLoadingState('loaded')
}
async function buyNft(nft) {
/* needs the user to sign the transaction, so will use Web3Provider and sign it */
const web3Modal = new Web3Modal()
const connection = await web3Modal.connect()
const provider = new ethers.providers.Web3Provider(connection)
const signer = provider.getSigner()
const contract = new ethers.Contract(nftmarketaddress, Market.abi, signer)
/* user will be prompted to pay the asking proces to complete the transaction */
const price = ethers.utils.parseUnits(nft.price.toString(), 'ether')
const transaction = await contract.createMarketSale(nftaddress, nft.tokenId, {
value: price
})
await transaction.wait()
loadNFTs()
}
if (loadingState === 'loaded' && !nfts.length) return (<h1 className="px-20 py-10 text-3xl">No items in marketplace</h1>)
return (
<div className="flex justify-center">
<div className="px-4" style={{ maxWidth: '1600px' }}>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 pt-4">
{
nfts.map((nft, i) => (
<div key={i} className="border shadow rounded-xl overflow-hidden">
<img src={nft.image} />
<div className="p-4">
<p style={{ height: '64px' }} className="text-2xl font-semibold">{nft.name}</p>
<div style={{ height: '70px', overflow: 'hidden' }}>
<p className="text-gray-400">{nft.description}</p>
</div>
</div>
<div className="p-4 bg-black">
<p className="text-2xl mb-4 font-bold text-white">{nft.price} ETH</p>
<button className="w-full bg-pink-500 text-white font-bold py-2 px-12 rounded" onClick={() => buyNft(nft)}>Buy</button>
</div>
</div>
))
}
</div>
</div>
</div>
)
}

Cuando la página se carga, consultamos el contrato inteligente en busca de los artículos que aún están a la venta y los mostramos en la pantalla junto con los metadatos sobre los artículos y un botón para comprarlos.

Creación y listado de artículos digitales

A continuación, vamos a crear la página que permita a los usuarios crear y listar activos digitales.

En esta página ocurren varias cosas:

  1. El usuario puede cargar y guardar archivos en IPFS
  2. El usuario puede crear un nuevo elemento digital único (NFT)
  3. El usuario puede establecer los metadatos y el precio del artículo y ponerlo a la venta en el marketplace

Una vez que el usuario crea y pone en venta un artículo, es redirigido a la página principal para ver todos los artículos en venta.

/* pages/create-item.js */
import { useState } from 'react'
import { ethers } from 'ethers'
import { create as ipfsHttpClient } from 'ipfs-http-client'
import { useRouter } from 'next/router'
import Web3Modal from 'web3modal'
const client = ipfsHttpClient('https://ipfs.infura.io:5001/api/v0')import {
nftaddress, nftmarketaddress
} from '../config'
import NFT from '../artifacts/contracts/NFT.sol/NFT.json'
import Market from '../artifacts/contracts/Market.sol/NFTMarket.json'
export default function CreateItem() {
const [fileUrl, setFileUrl] = useState(null)
const [formInput, updateFormInput] = useState({ price: '', name: '', description: '' })
const router = useRouter()
async function onChange(e) {
const file = e.target.files[0]
try {
const added = await client.add(
file,
{
progress: (prog) => console.log(`received: ${prog}`)
}
)
const url = `https://ipfs.infura.io/ipfs/${added.path}`
setFileUrl(url)
} catch (error) {
console.log('Error uploading file: ', error)
}
}
async function createMarket() {
const { name, description, price } = formInput
if (!name || !description || !price || !fileUrl) return
/* first, upload to IPFS */
const data = JSON.stringify({
name, description, image: fileUrl
})
try {
const added = await client.add(data)
const url = `https://ipfs.infura.io/ipfs/${added.path}`
/* after file is uploaded to IPFS, pass the URL to save it on Polygon */
createSale(url)
} catch (error) {
console.log('Error uploading file: ', error)
}
}
async function createSale(url) {
const web3Modal = new Web3Modal()
const connection = await web3Modal.connect()
const provider = new ethers.providers.Web3Provider(connection)
const signer = provider.getSigner()
/* next, create the item */
let contract = new ethers.Contract(nftaddress, NFT.abi, signer)
let transaction = await contract.createToken(url)
let tx = await transaction.wait()
let event = tx.events[0]
let value = event.args[2]
let tokenId = value.toNumber()
const price = ethers.utils.parseUnits(formInput.price, 'ether')
/* then list the item for sale on the marketplace */
contract = new ethers.Contract(nftmarketaddress, Market.abi, signer)
let listingPrice = await contract.getListingPrice()
listingPrice = listingPrice.toString()
transaction = await contract.createMarketItem(nftaddress, tokenId, price, { value: listingPrice })
await transaction.wait()
router.push('/')
}
return (
<div className="flex justify-center">
<div className="w-1/2 flex flex-col pb-12">
<input
placeholder="Asset Name"
className="mt-8 border rounded p-4"
onChange={e => updateFormInput({ ...formInput, name: e.target.value })}
/>
<textarea
placeholder="Asset Description"
className="mt-2 border rounded p-4"
onChange={e => updateFormInput({ ...formInput, description: e.target.value })}
/>
<input
placeholder="Asset Price in Eth"
className="mt-2 border rounded p-4"
onChange={e => updateFormInput({ ...formInput, price: e.target.value })}
/>
<input
type="file"
name="Asset"
className="my-4"
onChange={onChange}
/>
{
fileUrl && (
<img className="rounded mt-4" width="350" src={fileUrl} />
)
}
<button onClick={createMarket} className="font-bold mt-4 bg-pink-500 text-white rounded p-4 shadow-lg">
Create Digital Asset
</button>
</div>
</div>
)
}

Ver sólo los artículos comprados por el usuario

En el contrato inteligente Market.sol, creamos una función llamadafetchMyNFTs que sólo devuelve los artículos que posee el usuario.

En pages/my-assets.js, utilizaremos esa función para obtenerlos y representarlos.

Esta funcionalidad es diferente a la consulta de la página principal pages/index.js porque necesitamos pedirle al usuario su dirección y usarla en el contrato, por lo que el usuario tendrá que firmar la transacción para que pueda obtenerlos correctamente.

Ver el gist aquí

/* pages/my-assets.js */
import { ethers } from 'ethers'
import { useEffect, useState } from 'react'
import axios from 'axios'
import Web3Modal from "web3modal"

import {
nftmarketaddress, nftaddress
} from '../config'

import Market from '../artifacts/contracts/Market.sol/NFTMarket.json'
import NFT from '../artifacts/contracts/NFT.sol/NFT.json'

export default function MyAssets() {
const [nfts, setNfts] = useState([])
const [loadingState, setLoadingState] = useState('not-loaded')
useEffect(() => {
loadNFTs()
}, [])
async function loadNFTs() {
const web3Modal = new Web3Modal({
network: "mainnet",
cacheProvider: true,
})
const connection = await web3Modal.connect()
const provider = new ethers.providers.Web3Provider(connection)
const signer = provider.getSigner()

const marketContract = new ethers.Contract(nftmarketaddress, Market.abi, signer)
const tokenContract = new ethers.Contract(nftaddress, NFT.abi, provider)
const data = await marketContract.fetchMyNFTs()

const items = await Promise.all(data.map(async i => {
const tokenUri = await tokenContract.tokenURI(i.tokenId)
const meta = await axios.get(tokenUri)
let price = ethers.utils.formatUnits(i.price.toString(), 'ether')
let item = {
price,
tokenId: i.tokenId.toNumber(),
seller: i.seller,
owner: i.owner,
image: meta.data.image,
}
return item
}))
setNfts(items)
setLoadingState('loaded')
}
if (loadingState === 'loaded' && !nfts.length) return (<h1 className="py-10 px-20 text-3xl">No assets owned</h1>)
return (
<div className="flex justify-center">
<div className="p-4">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 pt-4">
{
nfts.map((nft, i) => (
<div key={i} className="border shadow rounded-xl overflow-hidden">
<img src={nft.image} className="rounded" />
<div className="p-4 bg-black">
<p className="text-2xl font-bold text-white">Price - {nft.price} Eth</p>
</div>
</div>
))
}
</div>
</div>
</div>
)
}

Panel de control del creador

La última página que vamos a crear es el panel de control del creador que les permitirá ver tanto todos los artículos que han creado como los que han vendido.
Esta página utilizará la función fetchItemsCreated del contrato inteligente Market.sol que devuelve sólo los artículos que coinciden con la dirección del usuario que hace la llamada a la función.
En el cliente, usamos el boolean sold para filtrar los artículos en otro array separado para mostrar al usuario sólo los artículos que han sido vendidos.

Crea un nuevo archivo llamado creator-dashboard.js en el directorio pages con el siguiente código:

/* pages/creator-dashboard.js */
import { ethers } from 'ethers'
import { useEffect, useState } from 'react'
import axios from 'axios'
import Web3Modal from "web3modal"

import {
nftmarketaddress, nftaddress
} from '../config'

import Market from '../artifacts/contracts/Market.sol/NFTMarket.json'
import NFT from '../artifacts/contracts/NFT.sol/NFT.json'

export default function CreatorDashboard() {
const [nfts, setNfts] = useState([])
const [sold, setSold] = useState([])
const [loadingState, setLoadingState] = useState('not-loaded')
useEffect(() => {
loadNFTs()
}, [])
async function loadNFTs() {
const web3Modal = new Web3Modal({
network: "mainnet",
cacheProvider: true,
})
const connection = await web3Modal.connect()
const provider = new ethers.providers.Web3Provider(connection)
const signer = provider.getSigner()

const marketContract = new ethers.Contract(nftmarketaddress, Market.abi, signer)
const tokenContract = new ethers.Contract(nftaddress, NFT.abi, provider)
const data = await marketContract.fetchItemsCreated()

const items = await Promise.all(data.map(async i => {
const tokenUri = await tokenContract.tokenURI(i.tokenId)
const meta = await axios.get(tokenUri)
let price = ethers.utils.formatUnits(i.price.toString(), 'ether')
let item = {
price,
tokenId: i.tokenId.toNumber(),
seller: i.seller,
owner: i.owner,
sold: i.sold,
image: meta.data.image,
}
return item
}))
/* create a filtered array of items that have been sold */
const soldItems = items.filter(i => i.sold)
setSold(soldItems)
setNfts(items)
setLoadingState('loaded')
}
if (loadingState === 'loaded' && !nfts.length) return (<h1 className="py-10 px-20 text-3xl">No assets created</h1>)
return (
<div>
<div className="p-4">
<h2 className="text-2xl py-2">Items Created</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 pt-4">
{
nfts.map((nft, i) => (
<div key={i} className="border shadow rounded-xl overflow-hidden">
<img src={nft.image} className="rounded" />
<div className="p-4 bg-black">
<p className="text-2xl font-bold text-white">Price - {nft.price} Eth</p>
</div>
</div>
))
}
</div>
</div>
<div className="px-4">
{
Boolean(sold.length) && (
<div>
<h2 className="text-2xl py-2">Items sold</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 pt-4">
{
sold.map((nft, i) => (
<div key={i} className="border shadow rounded-xl overflow-hidden">
<img src={nft.image} className="rounded" />
<div className="p-4 bg-black">
<p className="text-2xl font-bold text-white">Price - {nft.price} Eth</p>
</div>
</div>
))
}
</div>
</div>
)
}
</div>
</div>
)
}

Ejecución del proyecto

Para ejecutar el proyecto, necesitaremos un script de despliegue para desplegar los contratos inteligentes en la red de blockchain.

Desplegar los contratos en una red local

Cuando creamos el proyecto, Hardhat creó un script de despliegue de ejemplo en scripts/sample-script.js.

Para que el propósito de este script sea más claro, actualiza el nombre de scripts/sample-script.js a scripts/deploy.js.

A continuación, actualiza la función main en scripts/deploy.js con el siguiente código:

async function main() {
const NFTMarket = await hre.ethers.getContractFactory("NFTMarket");
const nftMarket = await NFTMarket.deploy();
await nftMarket.deployed();
console.log("nftMarket deployed to:", nftMarket.address);
const NFT = await hre.ethers.getContractFactory("NFT");
const nft = await NFT.deploy(nftMarket.address);
await nft.deployed();
console.log("nft deployed to:", nft.address);
}

Este script desplegará ambos contratos en la red blockchain.

Primero probaremos esto en una red local, y luego lo desplegaremos en la red de prueba de Mumbai.

Para poner en marcha una red local, abre tu terminal y ejecuta el siguiente comando:

npx hardhat node

Esto debería crear una red local con 19 cuentas.

A continuación, mantén el nodo en funcionamiento y abre una ventana de terminal separada para desplegar el contrato.

En una ventana separada, ejecuta el siguiente comando:

npx hardhat run scripts/deploy.js --network localhost

Cuando el despliegue se haya completado, la CLI debería imprimir las direcciones de los contratos que se desplegaron:

Usando estas direcciones, crea un nuevo archivo en la raíz del proyecto llamado config.js y añade el siguiente código, reemplazando el marcador de posición con las direcciones de los contratos impresas por la CLI:

export const nftmarketaddress = "market-contract-address"
export const nftaddress = "nft-contract-address"

Importación de cuentas a MetaMask

Puedes importar las cuentas creadas por el nodo a tu wallet Metamask para probarlas en la aplicación.

Cada una de estas cuentas está sembrada con 10000 ETH.

Para importar una de estas cuentas, primero cambia la red de tu wallet MetaMask a Localhost 8545.

A continuación, en MetaMask haz clic en Import Account (importar cuenta) en el menú de cuentas:

Copia y pega una de las Private Keys (claves privadas) que saca la CLI y haz clic en Importar. Una vez que la cuenta se importa, debes ver algunos de los Eth en la cuenta:

Sugeriría hacer esto con 2 o 3 cuentas para que tengas la posibilidad de probar las distintas funcionalidades entre los usuarios.

Ejecutar la aplicación

Ahora podemos probar la aplicación!.

Para iniciar la aplicación, ejecuta el siguiente comando en tu CLI:

npm run dev

Para probarlo todo, prueba a poner un artículo a la venta, y luego cambia a otra cuenta y cómpralo.

Despliegue en Polygon

Ahora que tenemos el proyecto en marcha y probado localmente, vamos a desplegarlo en Polygon. Empezaremos desplegando en Mumbai, la red de pruebas de Polygon.

Lo primero que tendremos que hacer es guardar una de las claves privadas de nuestra wallet en el archivo .secrets.

Para obtener la clave privada, puedes utilizar una de las claves privadas que te da Hardhat o puedes exportarlas directamente desde MetaMask.

  • En el mundo real sugiero no codificar nunca los valores de la clave privada en tus archivos como estamos haciendo aquí, sino establecerla como una variable de entorno local. Si entiendes cómo funcionan las variables de entorno, entonces usa esa ruta en su lugar. También recuerda que nunca debes enviar archivos secretos a Git.

Configuración de la red

A continuación, tenemos que cambiar de la red de prueba local a Mumbai Testnet.

Para ello, tenemos que crear y establecer la configuración de la red.

Primero, abre MetaMask y haz clic en Configuración (Setting).

A continuación, haz clic en Networks (Redes) y luego en Add Network (Añadir red):

Aquí, vamos a añadir las siguientes configuraciones para la red de prueba de Mumbai como se indica aquí:

Guarda esto, ¡y ya deberías poder cambiar y utilizar la nueva red!

Por último, necesitarás algunos tokens Matic de testnet para poder interactuar con las aplicaciones.

Para obtenerlos, puedes visitar Matic Faucet, introduciendo la dirección de las wallets que te gustaría solicitar los tokens.

Despliegue en la red Matic / Polygon

Ahora que tienes algunos tokens Matic, puedes desplegarlos en la red Polygon!.

Para ello, asegúrate de que la dirección asociada a la clave privada con la que estás desplegando tu contrato ha recibido algunos tokens Matic para poder pagar las tarifas de gas de la transacción.

Además, asegúrate de uncomment la configuración de mumbai en hardhat.config.js:

mumbai: {
url: "https://rpc-mumbai.maticvigil.com",
accounts: [privateKey]
}

Para desplegar en Matic, ejecuta el siguiente comando:

npx hardhat run scripts/deploy.js --network mumbai

Una vez desplegados los contratos, actualiza la llamada a la función loadNFTsen pages/index.js para incluir el nuevo endpoint RPC:

/* pages/index.js *//* old provider */
const provider = new ethers.providers.JsonRpcProvider()
/* new provider */
const provider = new ethers.providers.JsonRpcProvider("https://rpc-mumbai.maticvigil.com")

¡Ahora deberías poder actualizar las direcciones de los contratos en tu proyecto y probar en la nueva red 🎉!

npm run dev

Si te encuentras con un error, la dirección del contrato impresa en la consola por hardhat podría ser incorrecta debido a un error que he encontrado recientemente. Puedes obtener las direcciones correctas de los contratos visitando https://mumbai.polygonscan.com/ y pegando la dirección desde la que se desplegaron los contratos para ver las transacciones más recientes y obtener las direcciones de los contratos a partir de los datos de las transacciones.

Despliegue en la red principal

Para desplegar en la red principal de Matic / Polygon, puedes utilizar los mismos pasos que establecimos para la red de prueba de Mumbai.

La principal diferencia es que tendrás que utilizar un endpoint para Matic, así como importar la red en tu wallet MetaMask como se indica aquí.

Un ejemplo de actualización en tu proyecto para que esto suceda podría ser así:

/* hardhat.config.js */

/* adding Matic main network config to existing config */
...
matic: {
url: "https://rpc-mainnet.maticvigil.com",
accounts: [privateKey]
}
...

Las RPCs públicas como la mencionada anteriormente pueden tener límites de tráfico o de velocidad dependiendo del uso. Puedes registrarte para obtener una URL RPC gratuita dedicada utilizando servicios como Infura, MaticVigil, QuickNode, Alchemy, Chainstack o Ankr.

Por ejemplo, utilizando algo como Infura:

url: `https://polygon-mainnet.infura.io/v3/${infuraId}`

Próximos Pasos

Felicitaciones! Desplegaste una aplicación no trivial en Polygon.

Lo mejor de trabajar con soluciones como Polygon es el poco trabajo extra o aprendizaje que tuve que hacer en comparación con la construcción directa en Ethereum. Casi todas las API y las herramientas de estas Layer 2 (capas 2) y sidechains siguen siendo las mismas, lo que hace que cualquier habilidad sea transferible a varias plataformas como Polygon.

Para los próximos pasos, sugeriría portar las consultas implementadas en esta aplicación usando The Graph. The Graph abrirá muchos más patrones de acceso a los datos, incluyendo cosas como la paginación, el filtrado y la ordenación, que son necesarios para cualquier aplicación del mundo real.

También publicaré un tutorial mostrando cómo usar Polygon con The Graph en las próximas semanas.

--

--

Lorena Fabris
Lorena Fabris

Written by Lorena Fabris

Lawyer, Political Scientist, Blockchain Enthusiast

No responses yet