Creando y Utilizando un Mercado NFT Descentralizado con IPFS y Smart Contracts 🌐
Este tutorial te guiará paso a paso en la creación de un mercado NFT completamente descentralizado. Cubriremos la implementación de smart contracts en Solidity, el uso de IPFS para almacenar metadatos de los NFTs, y cómo interactuar con ellos para crear, listar y comprar activos digitales únicos. ¡Prepárate para construir tu propio OpenSea en miniatura!
Introducción al Mercado NFT Descentralizado ✨
Los NFTs (Tokens No Fungibles) han revolucionado la forma en que pensamos sobre la propiedad digital. Sin embargo, muchos mercados NFT populares aún dependen de infraestructuras centralizadas para almacenar metadatos o para la interfaz de usuario. En este tutorial, daremos un paso más allá para crear un mercado verdaderamente descentralizado, utilizando el poder de los smart contracts de Ethereum (o una EVM compatible) y el Sistema de Archivos InterPlanetario (IPFS).
¿Por qué Descentralizar un Mercado NFT? 💡
La descentralización ofrece múltiples beneficios:
- Resistencia a la censura: Los datos y la lógica de la aplicación no pueden ser alterados o eliminados por una única entidad.
- Transparencia: Todas las transacciones y la información del NFT son verificables en la blockchain.
- Inmutabilidad: Una vez que un NFT se registra en la blockchain y sus metadatos en IPFS, no se pueden cambiar.
- Confianza: Se reduce la necesidad de intermediarios, ya que las operaciones se rigen por código auditable.
Componentes Clave de Nuestro Mercado NFT 🛠️
Para construir nuestro mercado, necesitaremos los siguientes componentes:
- Smart Contract de NFT (ERC-721): Para definir la propiedad y las características de nuestros tokens no fungibles.
- Smart Contract del Mercado: Para gestionar la lógica de listado, compra y venta de NFTs.
- IPFS: Para almacenar los metadatos de los NFTs (nombre, descripción, URL de la imagen, etc.) de forma descentralizada.
- Una Cartera Web3 (ej. MetaMask): Para interactuar con la blockchain.
- Una Herramienta de Desarrollo (ej. Hardhat/Foundry): Para desplegar y probar nuestros contratos.
Diagrama de Flujo del Mercado NFT
Aquí tienes un diagrama simplificado de cómo interactuarán los componentes:
1. El Smart Contract ERC-721 para NFTs 🖼️
Comenzaremos creando nuestro propio contrato ERC-721. Utilizaremos las implementaciones estándar de OpenZeppelin por su seguridad y robustez.
Inicializando el Proyecto (Hardhat/Foundry) 🚀
Si usas Hardhat:
npx hardhat init
npm install @openzeppelin/contracts
Si usas Foundry:
forge init
forge install OpenZeppelin/openzeppelin-contracts
Código del Smart Contract ERC-721 (MyNFT.sol) 📝
Este contrato nos permitirá crear (mintear) NFTs y asignarles un URI de metadatos.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract MyNFT is ERC721, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIdCounter;
constructor(address initialOwner) ERC721("My Awesome NFT", "MANFT") Ownable(initialOwner) {}
function safeMint(address to, string memory uri) public onlyOwner returns (uint256) {
_tokenIdCounter.increment();
uint256 newItemId = _tokenIdCounter.current();
_safeMint(to, newItemId);
_setTokenURI(newItemId, uri);
return newItemId;
}
// La función base URI puede ser útil para construir URIs si los metadatos tienen una base común.
// En nuestro caso, pasamos el URI completo en `safeMint`.
// function _baseURI() internal pure override returns (string memory) {
// return "ipfs://"; // Ejemplo si todos los NFTs usan el mismo prefijo IPFS
// }
}
2. El Smart Contract del Mercado NFT 🛒
Este contrato gestionará la lógica de listado, compra y cancelación de NFTs. Actuará como un escrow (depósito de garantía) para los NFTs y los fondos.
Código del Smart Contract (NFTMarketplace.sol) 💰
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract NFTMarketplace is Ownable, IERC721Receiver {
struct Listing {
uint256 itemId;
address nftContract;
uint256 tokenId;
uint256 price;
address seller;
bool isSold;
}
mapping(uint256 => Listing) public listings;
uint256 private _listingIdCounter; // Usaremos esto para generar IDs únicos para cada listado
event ItemListed(uint256 indexed listingId, address indexed nftContract, uint256 indexed tokenId, address seller, uint256 price);
event ItemBought(uint256 indexed listingId, address indexed nftContract, uint256 indexed tokenId, address buyer, uint256 price);
event ListingCancelled(uint256 indexed listingId, address indexed nftContract, uint256 indexed tokenId, address seller);
constructor(address initialOwner) Ownable(initialOwner) {}
function listItem(address _nftContract, uint256 _tokenId, uint256 _price) public {
require(_price > 0, "Price must be greater than zero");
IERC721 nft = IERC721(_nftContract);
require(nft.ownerOf(_tokenId) == msg.sender, "You do not own this NFT");
// Transferir la propiedad del NFT al contrato del marketplace
nft.transferFrom(msg.sender, address(this), _tokenId);
_listingIdCounter++;
uint256 newListingId = _listingIdCounter;
listings[newListingId] = Listing({
itemId: newListingId,
nftContract: _nftContract,
tokenId: _tokenId,
price: _price,
seller: msg.sender,
isSold: false
});
emit ItemListed(newListingId, _nftContract, _tokenId, msg.sender, _price);
}
function buyItem(uint256 _listingId) public payable {
Listing storage listing = listings[_listingId];
require(listing.itemId != 0, "Listing does not exist");
require(!listing.isSold, "Item already sold");
require(msg.value == listing.price, "Please submit the asking price");
require(listing.seller != msg.sender, "You cannot buy your own item");
// Marcar como vendido
listing.isSold = true;
// Transferir el NFT al comprador
IERC721(listing.nftContract).transferFrom(address(this), msg.sender, listing.tokenId);
// Enviar el pago al vendedor
payable(listing.seller).transfer(msg.value);
emit ItemBought(_listingId, listing.nftContract, listing.tokenId, msg.sender, msg.value);
}
function cancelListing(uint256 _listingId) public {
Listing storage listing = listings[_listingId];
require(listing.itemId != 0, "Listing does not exist");
require(!listing.isSold, "Item already sold");
require(listing.seller == msg.sender, "You are not the seller of this item");
// Transferir el NFT de vuelta al vendedor
IERC721(listing.nftContract).transferFrom(address(this), msg.sender, listing.tokenId);
// Eliminar o invalidar el listado
delete listings[_listingId]; // O marcar como cancelado
emit ListingCancelled(_listingId, listing.nftContract, listing.tokenId, msg.sender);
}
// Función para recibir NFTs (requerida por IERC721Receiver)
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external pure override returns (bytes4) {
return IERC777Receiver.onERC721Received.selector;
}
// Función para retirar fondos en caso de emergencia o para el propietario
function withdraw() public onlyOwner {
payable(owner()).transfer(address(this).balance);
}
}
Tabla de Funciones del Marketplace 📋
| Función | Descripción | Permisos | Evento Emitido |
|---|---|---|---|
listItem | Permite a un propietario listar su NFT para la venta. El NFT se transfiere al contrato. | Propietario del NFT | ItemListed |
buyItem | Permite a un comprador adquirir un NFT listado, pagando el precio especificado. | Cualquier usuario | ItemBought |
cancelListing | Permite al vendedor retirar su NFT del listado si no ha sido vendido. | Vendedor del NFT | ListingCancelled |
onERC721Received | Interfaz requerida para que el contrato pueda recibir NFTs. | Llamado por transferFrom | Ninguno |
withdraw | Permite al propietario del contrato retirar cualquier Ether accidentalmente enviado. | onlyOwner | Ninguno |
3. Almacenando Metadatos en IPFS 🌍
Los metadatos del NFT son cruciales: definen el nombre, la descripción, la imagen y otras propiedades del activo digital. Almacenarlos en IPFS garantiza que sean inmutables y resistentes a la censura.
¿Qué es IPFS? 🤔
IPFS es un protocolo peer-to-peer para almacenar y compartir datos en un sistema de archivos distribuido. A diferencia de HTTP, que localiza contenido por su ubicación (un servidor específico), IPFS lo hace por su contenido (un hash criptográfico). Esto significa que si el contenido está disponible en cualquier nodo de la red IPFS, se puede acceder a él.
Pasos para Subir a IPFS 🚀
- Crea un archivo JSON de metadatos: Sigue el estándar de metadatos de OpenSea/ERC-721.
- Sube la imagen: Sube el archivo de imagen de tu NFT a IPFS para obtener su
ipfs://URI. - Sube el JSON: Sube el archivo JSON de metadatos (que ya contiene la URI de la imagen) a IPFS para obtener su
ipfs://URI final.
Ejemplo de Archivo JSON de Metadatos (my_nft_metadata.json)
{
"name": "Atardecer en la Playa Descentralizada",
"description": "Una obra de arte digital que captura la belleza de un atardecer imaginario, almacenada de forma inmutable en la red IPFS.",
"image": "ipfs://QmTfQfHwE8Y32d4R5u6gX3n2m1j7pL9cK0oV6zA1b2c3d4/sunset.jpg",
"attributes": [
{
"trait_type": "Atmósfera",
"value": "Pacífica"
},
{
"trait_type": "Color Dominante",
"value": "Naranja"
}
]
}
Cómo usar Pinata para subir archivos a IPFS
- Regístrate en Pinata: Crea una cuenta en pinata.cloud.
- Crea una API Key: Ve a 'API Keys' y genera una nueva clave.
- Sube archivos: Puedes usar la interfaz web o su SDK/API.
- Subir imagen: Sube tu archivo
sunset.jpg. Pinata te devolverá unCID(Content Identifier). Tu URI de imagen seráipfs://<CID>/sunset.jpg. - Actualizar JSON: Modifica
my_nft_metadata.jsoncon la URI de la imagen obtenida. - Subir JSON: Sube el archivo JSON actualizado. Pinata te dará otro
CID. Tu URI final para el NFT seráipfs://<CID>/my_nft_metadata.json.
- Subir imagen: Sube tu archivo
Este URI final es el que pasarás a la función safeMint de tu contrato MyNFT.
4. Despliegue y Pruebas 🧪
Una vez que tenemos los contratos y entendemos cómo manejar los metadatos, es hora de desplegar y probar.
Script de Despliegue (ej. Hardhat) ⚙️
Crea un archivo de despliegue, por ejemplo, scripts/deploy.js:
// scripts/deploy.js
const hre = require("hardhat");
async function main() {
const [deployer] = await hre.ethers.getSigners();
console.log("Desplegando contratos con la cuenta:", deployer.address);
const MyNFT = await hre.ethers.getContractFactory("MyNFT");
const myNFT = await MyNFT.deploy(deployer.address);
await myNFT.waitForDeployment();
console.log("MyNFT desplegado en:", await myNFT.getAddress());
const NFTMarketplace = await hre.ethers.getContractFactory("NFTMarketplace");
const nftMarketplace = await NFTMarketplace.deploy(deployer.address);
await nftMarketplace.waitForDeployment();
console.log("NFTMarketplace desplegado en:", await nftMarketplace.getAddress());
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Ejecuta el script de despliegue en tu red de desarrollo o testnet:
npx hardhat run scripts/deploy.js --network localhost # o goerli, sepolia, etc.
Interacción con los Contratos (Hardhat Console / Ethers.js) 🌐
Ahora vamos a interactuar paso a paso:
Primero, el dueño del contrato `MyNFT` mintea un nuevo token, apuntando a nuestros metadatos IPFS.
El propietario del NFT debe aprobar que el contrato del Marketplace pueda transferir su NFT. Esto es crucial para la función `listItem`.
El propietario del NFT lista el token en el Marketplace con un precio. El NFT se transfiere al contrato del Marketplace.
Otro usuario (comprador) paga el precio especificado para adquirir el NFT. El Marketplace transfiere el NFT al comprador y el Ether al vendedor.
Ejemplo de Interacción en Hardhat Console
// Asegúrate de haber desplegado los contratos y tener sus direcciones
// npx hardhat console --network localhost
const MyNFT = await ethers.getContractFactory("MyNFT");
const myNFT = await MyNFT.attach("DIRECCION_DE_MYNFT"); // Reemplaza con la dirección real
const NFTMarketplace = await ethers.getContractFactory("NFTMarketplace");
const nftMarketplace = await NFTMarketplace.attach("DIRECCION_DE_MARKETPLACE"); // Reemplaza con la dirección real
const [deployer, buyer] = await ethers.getSigners(); // Obtener diferentes cuentas
// Paso 1: Mintear un NFT (deployer es el owner en este ejemplo)
const metadataURI = "ipfs://bafyreigv62o7qg5m32p45l7v5k2k6c2x6z4q7g1h3j5k6l7m8n9o0p1q2r3s4t5u6v7w8x9y0z/"; // Tu URI de metadatos IPFS
let tx = await myNFT.connect(deployer).safeMint(deployer.address, metadataURI);
await tx.wait();
const tokenId = (await myNFT.tokenOfOwnerByIndex(deployer.address, 0)).toNumber();
console.log(`NFT minteado con ID: ${tokenId} al owner: ${deployer.address}`);
// Paso 2: Aprobar el Marketplace para que pueda mover el NFT
tx = await myNFT.connect(deployer).approve(await nftMarketplace.getAddress(), tokenId);
await tx.wait();
console.log("Marketplace aprobado para mover el NFT.");
// Paso 3: Listar el NFT por el deployer (vendedor)
const price = ethers.parseEther("0.1"); // 0.1 ETH
tx = await nftMarketplace.connect(deployer).listItem(await myNFT.getAddress(), tokenId, price);
await tx.wait();
console.log("NFT listado en el marketplace.");
// Recuperar el listingId del evento
const filter = nftMarketplace.filters.ItemListed(null, await myNFT.getAddress(), tokenId);
const events = await nftMarketplace.queryFilter(filter);
const listingId = events[0].args.listingId;
console.log(`Listing ID del NFT: ${listingId}`);
// Paso 4: Comprar el NFT por el buyer
tx = await nftMarketplace.connect(buyer).buyItem(listingId, { value: price });
await tx.wait();
console.log(`NFT con ID ${tokenId} comprado por ${buyer.address}.`);
// Verificar el nuevo propietario del NFT
const newOwner = await myNFT.ownerOf(tokenId);
console.log(`El nuevo propietario del NFT ${tokenId} es: ${newOwner}`);
// Debería ser buyer.address
// Intentar cancelar (debería fallar si ya está vendido)
// await nftMarketplace.connect(deployer).cancelListing(listingId);
Consideraciones Adicionales y Próximos Pasos 🌟
- Interfaz de Usuario (Frontend): Para que este mercado sea realmente usable, necesitarías una interfaz de usuario (dApp) construida con React, Vue, Angular o Next.js, utilizando bibliotecas como Ethers.js o Web3.js para interactuar con los contratos.
- Moneda de Pago: Este ejemplo usa Ether. Podrías modificar el contrato para aceptar tokens ERC-20 como USDC o DAI.
- Comisiones: Un mercado real aplicaría una comisión por venta, que se distribuiría al propietario del contrato o a una DAO. Esto implicaría una lógica adicional en
buyItem. - Búsqueda y Filtrado: Para una experiencia de usuario completa, necesitarías indexar los eventos de la blockchain y los metadatos de IPFS en una base de datos centralizada (o una solución descentralizada como The Graph) para permitir búsquedas y filtros eficientes.
- Seguridad: Auditar los contratos es fundamental antes de desplegarlos en una red principal. Considera patrones de seguridad como Re-entrancy Guard.
¿Qué es The Graph y por qué es útil aquí?
The Graph es un protocolo descentralizado para indexar y consultar datos de blockchain. En lugar de que tu frontend tenga que leer directamente de la blockchain o de IPFS (lo cual puede ser lento o complejo para datos agregados), puedes desplegar un subgraph que escucha eventos de tus contratos (como ItemListed, ItemBought) y los guarda en un formato consultable (GraphQL). Esto permite que tu interfaz de usuario muestre listas de NFTs, historial de ventas, etc., de manera rápida y eficiente.
¡Felicidades! 🎉
Has aprendido los fundamentos para construir un mercado NFT descentralizado, desde los smart contracts hasta el almacenamiento de metadatos en IPFS y la lógica de interacción básica. ¡Este es un gran paso en tu viaje Web3!
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!