Los NFT se han convertido en una parte fundamental del ecosistema web3 desde el boom de 2021, evolucionando más allá de los coleccionables hacia casos de uso en el mundo real, como acceso a membresías, tokenización de activos, objetos de videojuegos e identidad digital.
Las plataformas sin código como Phosphor han facilitado más que nunca que creadores y desarrolladores acuñen, personalicen e implementen NFT a través de un intuitivo Creator Studio y un conjunto de APIs, eliminando la necesidad de construir contratos NFT personalizados desde cero. Sin embargo, para los desarrolladores que buscan profundizar su comprensión de Solidity y las experiencias nativas de web3, aprender a construir un marketplace de NFT sigue siendo una habilidad fundamental.
En esta guía, recorreremos el proceso de desarrollo e implementación de un marketplace de NFT básico pero completamente funcional en Linea. Al finalizar, tendrás los conocimientos necesarios para lanzar tu propio marketplace y adquirir un dominio más sólido del desarrollo de contratos inteligentes, aplicaciones descentralizadas e interacciones web3.
Requisitos previos
Debes tener un conocimiento básico de Solidity.
Debes tener Nodejs y Foundry instalados en tu PC.
Descripción general del contrato
Este marketplace de NFT admitirá las siguientes funcionalidades:
Listar NFT en venta: Los usuarios pueden establecer un precio y listar sus NFT, con los detalles almacenados de forma segura en el contrato inteligente.
Comprar NFT: Los compradores pueden adquirir NFT listados pagando el Ether requerido, garantizando una transacción fluida.
Compra por lotes de NFT: Se pueden comprar múltiples NFT en una sola transacción para reducir los costos de gas.
Retirar NFT del listado: Los propietarios pueden eliminar sus NFT de la venta, devolviéndolos a sus billeteras.
Configuración del proyecto
Utilizaremos el framework Foundry para este proyecto. Con Foundry instalado, ejecuta el siguiente comando para iniciar un nuevo proyecto:
forge init marketAbre la carpeta market en Vscode o tu editor de código favorito, y elimina los archivos scripts/counter.s.sol, src/counter.sol y test/counter.t.sol.
Instalar todas las dependencias
forge install foundry-rs/forge-std --no-commit && forge install OpenZeppelin/openzeppelin-contracts --no-commitCódigo completo del contrato inteligente del marketplace de NFT
Crea un archivo market.sol en la carpeta src y añade el código a continuación al archivo.
pragma solidity ^0.8.18;
import "lib/openzeppelin-contracts/contracts/token/ERC721/IERC721.sol";
import {Test, console} from "forge-std/Test.sol";
contract NFTMarketplace {
struct Listing {
address seller;
uint256 price;
}
mapping(address => mapping(uint256 => Listing)) public listings;
event Listed(address indexed nftContract, uint256 indexed tokenId, address seller, uint256 price);
event Purchase(address indexed nftContract, uint256 indexed tokenId, address buyer, uint256 price);
event Delisted(address indexed nftContract, uint256 indexed tokenId, address seller);
modifier isSeller(address nftContract, uint256 tokenId, address spender) {
require(listings[address(nftContract)][tokenId].seller == spender, "Not the seller");
_;
}
modifier isListed(address nftContract, uint256 tokenId) {
require(listings[nftContract][tokenId].price > 0, "Not listed");
_;
}
modifier notListed(address nftContract, uint256 tokenId) {
require(listings[nftContract][tokenId].price == 0, "Already listed");
_;
}
function listNFT(address nftContract, uint256 tokenId, uint256 price)
external
notListed(nftContract, tokenId)
{
require(price > 0, "Price must be greater than zero");
require(IERC721(nftContract).ownerOf(tokenId) == msg.sender, "not owner");
IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId);
listings[nftContract][tokenId] = Listing(msg.sender, price);
emit Listed(nftContract, tokenId, msg.sender, price);
}
function buyNFT(address nftContract, uint256 tokenId)
public
payable
isListed(nftContract, tokenId)
{
Listing memory listing = listings[nftContract][tokenId];
require(msg.value >= listing.price, "Insufficient funds");
delete listings[nftContract][tokenId];
(bool sent, ) = listing.seller.call{value: listing.price}("");
require(sent, "Failed to send Ether");
IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId);
emit Purchase(nftContract, tokenId, msg.sender, listing.price);
}
function buyMultipleNFT(address nftContract, uint256 [] memory tokenIds)
external
payable
{
uint256 totalAmount;
for (uint256 i; i < tokenIds.length; i++){
Listing memory listing = listings[nftContract][tokenIds[i]];
totalAmount += listing.price;
}
require(msg.value >= totalAmount, "Insufficient funds");
for (uint256 i; i < tokenIds.length; i++){
buyNFT(nftContract, tokenIds[i]);
}
}
function delistNFT(address nftContract, uint256 tokenId)
external
isListed(nftContract, tokenId)
isSeller(nftContract, tokenId, msg.sender)
{
Listing memory listing = listings[nftContract][tokenId];
delete listings[nftContract][tokenId];
IERC721(nftContract).transferFrom(address(this), listing.seller, tokenId);
emit Delisted(nftContract, tokenId, listing.seller);
}
}
Características clave del código
Struct de listado
Esto es esencial para implementar funcionalidades como listar, comprar y retirar del listado en un marketplace de NFT descentralizado. Proporciona una forma clara y eficiente de gestionar los listados, asegurando que el marketplace mantenga un registro del vendedor y el precio de cada artículo, ambos cruciales para su funcionamiento.
struct Listing {
address seller;
uint256 price;
}
Mapping de listados
Esta línea de código define un mapping anidado en Solidity, que organiza y almacena los listados del marketplace de NFT. El mapping externo utiliza la dirección del contrato NFT como clave, lo que permite al contrato inteligente categorizar los listados según sus respectivas colecciones de NFT. Esto garantiza que el marketplace pueda admitir múltiples colecciones de NFT.
El mapping interno utiliza el token ID (un identificador único para cada NFT) como clave, lo que permite una gestión eficiente de múltiples listados bajo un mismo contrato NFT.
mapping(address => mapping(uint256 => Listing)) public listings;
Listado de NFT
La función listNFT permite a los usuarios listar sus NFT en venta en el marketplace. Recibe tres parámetros: el contrato NFT, el token ID y el precio. El contrato NFT hace referencia a la dirección de la colección desde la cual el usuario desea listar un NFT, el token ID es el identificador único del NFT dentro de esa colección, y el precio es el monto por el que el usuario desea vender el NFT.
La función primero valida que el usuario sea el propietario del NFT y verifica que el precio sea mayor que cero. Una vez superadas estas comprobaciones, el NFT se transfiere del vendedor al marketplace, se actualiza el mapping de listados y se emite el evento Listed con los parámetros correctos.
function listNFT(address nftContract, uint256 tokenId, uint256 price)
external
notListed(nftContract, tokenId)
{
require(price > 0, "Price must be greater than zero");
require(IERC721(nftContract).ownerOf(tokenId) == msg.sender, "not owner");
IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId);
listings[nftContract][tokenId] = Listing(msg.sender, price);
emit Listed(nftContract, tokenId, msg.sender, price);
}
Compra de NFT
La función buyNFT permite a los usuarios comprar un NFT listado en el marketplace. Recibe dos parámetros: nftContract, la dirección del contrato inteligente del NFT, y tokenId, el identificador único del NFT que se está comprando.
La función primero verifica que el comprador haya enviado suficiente Ether para cubrir el precio del NFT. Una vez validado, el listado se elimina del mapping de listados, el pago se transfiere de forma segura al vendedor y el NFT se transfiere desde el contrato del marketplace a la dirección del comprador, finalizando la compra.
function buyNFT(address nftContract, uint256 tokenId)
public
payable
isListed(nftContract, tokenId)
{
Listing memory listing = listings[nftContract][tokenId];
require(msg.value >= listing.price, "Insufficient funds");
delete listings[nftContract][tokenId];
(bool sent, ) = listing.seller.call{value: listing.price}("");
require(sent, "Failed to send Ether");
IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId);
emit Purchase(nftContract, tokenId, msg.sender, listing.price);
}
Compra de múltiples NFT
La función buyMultipleNFT permite a un usuario comprar múltiples NFT en una sola transacción. Primero calcula el precio total sumando los precios de todos los NFT listados en el array tokenIds. Luego verifica que el comprador haya enviado fondos suficientes para cubrir el monto total.
Una vez validados los fondos, la función itera a través del array tokenIds, llamando a la función buyNFT para cada NFT. Esto garantiza que cada NFT se compre de forma individual, el pago se transfiera al vendedor y los NFT se envíen al comprador, siguiendo el mismo proceso que la función buyNFT.
function buyMultipleNFT(address nftContract, uint256 [] memory tokenIds)
external
payable
{
uint256 totalAmount;
for (uint256 i; i < tokenIds.length; i++){
Listing memory listing = listings[nftContract][tokenIds[i]];
totalAmount += listing.price;
}
require(msg.value >= totalAmount, "Insufficient funds");
for (uint256 i; i < tokenIds.length; i++){
buyNFT(nftContract, tokenIds[i]);
}
}
Retirar NFT del listado
La función delistNFT permite a un vendedor eliminar su NFT del marketplace. Primero verifica que el NFT esté listado y que quien realiza la llamada sea el propietario legítimo. Una vez validado, el listado se elimina del marketplace, retirando efectivamente el NFT de la venta.
La función luego transfiere el NFT de vuelta al vendedor, asegurando que recupere la propiedad de su activo. Finalmente, se emite el evento Delisted para confirmar que el NFT ha sido eliminado exitosamente del marketplace.
function delistNFT(address nftContract, uint256 tokenId)
external
isListed(nftContract, tokenId)
isSeller(nftContract, tokenId, msg.sender)
{
Listing memory listing = listings[nftContract][tokenId];
delete listings[nftContract][tokenId];
IERC721(nftContract).transferFrom(address(this), listing.seller, tokenId);
emit Delisted(nftContract, tokenId, listing.seller);
}
Eventos clave
El contrato emite eventos para las acciones esenciales del marketplace:
Listed: Se activa cuando se lista un NFT, registrando detalles como el vendedor, el precio y la información del NFT.
Purchase: Se emite cuando un comprador adquiere un NFT, garantizando la trazabilidad de la transacción.
Delisted: Indica que un NFT ha sido eliminado exitosamente del marketplace, manteniendo la transparencia.
event Listed(address indexed nftContract, uint256 indexed tokenId, address seller, uint256 price);
event Purchase(address indexed nftContract, uint256 indexed tokenId, address buyer, uint256 price);
event Delisted(address indexed nftContract, uint256 indexed tokenId, address seller
Descripción general del conjunto de pruebas
Crea un archivo NFT.sol en la carpeta src y añade el código a continuación al archivo.
pragma solidity ^0.8.18;
import "lib/openzeppelin-contracts/contracts/token/ERC721/ERC721.sol";
import "lib/openzeppelin-contracts/contracts/access/Ownable.sol";
contract NFT is ERC721, Ownable {
uint256 private _tokenIdCounter;
string private _baseTokenURI;
event Minted(address indexed to, uint256 tokenId);
constructor(string memory name, string memory symbol, string memory baseTokenURI) ERC721(name, symbol) Ownable(msg.sender) {
_baseTokenURI = baseTokenURI;
}
function mint(address to) external onlyOwner {
uint256 tokenId = _tokenIdCounter;
_tokenIdCounter += 1;
_safeMint(to, tokenId);
emit Minted(to, tokenId);
}
function _baseURI() internal view override returns (string memory) {
return _baseTokenURI;
}
function setBaseTokenURI(string memory newBaseTokenURI) external onlyOwner {
_baseTokenURI = newBaseTokenURI;
}
function getTokenCount() external view returns (uint256) {
return _tokenIdCounter;
}
}
Crea un archivo market.t.sol en la carpeta test y añade el código a continuación al archivo.
pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol";
import {NFTMarketplace} from "../src/Market.sol";
import {NFT} from "../src/NFT.sol";
contract MarketTest is Test {
NFTMarketplace public market;
NFT public nft;
address public user1 = makeAddr("user1");
address public user2 = makeAddr("user2");
address public user3 = makeAddr("user3");
function setUp() public {
nft = new NFT("TestNFT", "tnft", "");
market = new NFTMarketplace();
nft.mint(user1);
nft.mint(user1);
nft.mint(user1);
}
function test_Listing() public {
vm.startPrank(user1);
nft.approve(address(market), 0);
market.listNFT(address(nft), 0, 1 ether);
}
function test_BuyingNFT() public {
vm.startPrank(user1);
nft.approve(address(market), 0);
market.listNFT(address(nft), 0, 1 ether);
vm.startPrank(user2);
deal(user2, 10 ether);
market.buyNFT{value: 1 ether}(address(nft), 0);
assert(nft.ownerOf(0) == user2);
}
function test_BuyingMultipleNFT() public {
vm.startPrank(user1);
nft.approve(address(market), 0);
nft.approve(address(market), 1);
nft.approve(address(market), 2);
market.listNFT(address(nft), 0, 1 ether);
market.listNFT(address(nft), 1, 1 ether);
market.listNFT(address(nft), 2, 1 ether);
vm.startPrank(user2);
deal(user2, 10 ether);
uint256 [] memory nfts = new uint256[] (3);
nfts[0] = 0;
nfts[1] = 1;
nfts[2] = 2;
market.buyMultipleNFT{value: 3 ether}(address(nft), nfts);
}
function test_DelistingNFT() public {
vm.startPrank(user1);
nft.approve(address(market), 0);
market.listNFT(address(nft), 0, 1 ether);
assert(nft.ownerOf(0) == address(market));
market.delistNFT(address(nft), 0);
assert(nft.ownerOf(0) == address(user1));
}
}
El conjunto de pruebas anterior garantiza que el contrato del marketplace de NFT funcione según lo previsto en todas sus funcionalidades principales. A continuación, se detalla cada prueba:
Configuración del entorno de pruebas
La función setUp inicializa el entorno desplegando tanto el contrato NFTMarketplace como el contrato NFT. Acuña tres NFT para user1 con fines de prueba. Se despliega un contrato NFT simulado (NFT) con una función mint para generar NFT, mientras que el contrato del marketplace (NFTMarketplace) se instancia. User1 es propietario de todos los NFT acuñados, lo que permite probar las funcionalidades de listado y retiro del listado.
Prueba de listado de NFT
La función test_Listing verifica que user1 pueda listar un NFT en venta en el marketplace.
User1 aprueba al marketplace para transferir el tokenId 0.
User1 lista el NFT a un precio de 1 Ether.
La prueba confirma que la función listNFT funciona correctamente y que el NFT se transfiere al marketplace.
Prueba de compra de un solo NFT
La función test_BuyingNFT simula la compra de un NFT listado por parte de user2.
User1 lista el tokenId 0 por 1 Ether.
User2 recibe 10 Ether y llama a la función buyNFT, enviando 1 Ether para comprar el NFT.
Tras la compra, la prueba verifica que user2 es el nuevo propietario del tokenId 0.
Prueba de compra de múltiples NFT
La función test_BuyingMultipleNFT prueba la capacidad de comprar múltiples NFT en una sola transacción.
User1 lista tres NFT (tokenId 0, 1, 2) por 1 Ether cada uno.
User2 llama a buyMultipleNFT, pasando los token IDs y enviando 3 Ether.
La prueba verifica que user2 se convierte en propietario de los tres NFT y que el marketplace procesa correctamente las compras múltiples.
Prueba de retiro de NFT del listado
La función test_DelistingNFT verifica que user1 pueda retirar su NFT del marketplace.
User1 lista el tokenId 0 por 1 Ether y llama a delistNFT.
El NFT se devuelve a user1, y la prueba confirma que la propiedad ha vuelto a user1.
Ejecutar pruebas
forge testDespliegue y verificación en Linea
Añade las variables requeridas al archivo .env, asegurándote de que esté ubicado en el nivel raíz del proyecto:
LINEA_RPC_URL=https://linea-mainnet.infura.io/v3/
PRIVATE_KEY=*************************************************
Añade los detalles a continuación en foundry.toml
[etherscan]
linea = { key = "${LINEA_API_KEY}", url = "https://api-sepolia.lineascan.build/api" }
Crea un Makefile y añade los detalles a continuación:
deploy:
forge create src/Market.sol:NFTMarketplace --rpc-url $(LINEA_RPC_URL) --private-key $(PRIVATE_KEY)
verify:; forge verify-contract --rpc-url $(LINEA_RPC_URL) --chain linea <contract address> src/Market.sol:NFTMarketplace
Despliegue
make deployVerificación
make verifyConclusión
Construir un marketplace de NFT en Linea brinda a los desarrolladores experiencia práctica en el desarrollo de contratos inteligentes, programación en Solidity e interacciones web3. En esta guía, cubrimos las funcionalidades principales de un marketplace descentralizado, incluyendo el listado, la compra, la compra por lotes y el retiro del listado de NFT, garantizando transacciones seguras y eficientes.
También recorrimos la implementación y las pruebas del contrato inteligente utilizando Foundry, así como el proceso de despliegue y verificación en Linea. Siguiendo esta guía, ahora cuentas con los conocimientos fundamentales para construir, probar e implementar tu propio marketplace de NFT.
Para más tutoriales prácticos y análisis en profundidad sobre el desarrollo blockchain, visita el canal de YouTube de MetaMask Developer y mantente al día con nuestras últimas novedades siguiéndonos en Twitter.