Los marketplaces son una parte fundamental de todo proyecto de NFTs. Pero ¿cuándo deberíamos usar OpenSea y cuándo nuestro propio Marketplace? Pues necesitaremos crear uno nuestro en caso que la red que en la red que estemos usando no exista un marketplace predominante o en el caso que necesitemos mecanismos avanzados como es el caso normalmente en los juegos Play to Earn. En este video explicaremos todo lo necesario para crear un marketplace. Desde los smart contracts hasta la página web.
Dependencias
Para este tutorial ocuparás NodeJs que recomiendo descargarlo en Linux via NVM , también necesitarás un URL de RPC te recomiendo usar INFURA, y finalmente Metamask con fondos de Rinkeby Testnet que puedes conseguir desde el Faucet.
1. Los smart contracts
Lanza los siguientes 2 contratos.
Uno es el contrato de los NFTs.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.11;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
contract MyNFT is ERC721, ERC721Enumerable {
uint public supply;
constructor() ERC721("Dummy Token", "DT") {}
function mint() public
{
_mint(msg.sender, supply);
supply += 1;
}
function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721Enumerable) returns (bool)
{
return super.supportsInterface(interfaceId);
}
function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal override(ERC721, ERC721Enumerable)
{
super._beforeTokenTransfer(from, to, tokenId);
}
}
El otro es el del Marketplace. Recuerda reemplazar el 0x0000000000000000000000000000000000000000
por el address de los NFTs.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.11;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
struct Listing
{
address owner;
bool is_active;
uint token_id;
uint price;
}
contract Marketplace {
using SafeMath for uint256;
uint public listing_count = 0;
mapping (uint => Listing) public listings;
ERC721 token_contract = ERC721(0x0000000000000000000000000000000000000000);
function addListing(uint token_id, uint price) public
{
listings[listing_count] = Listing(msg.sender, true, token_id, price);
listing_count = listing_count.add(1);
token_contract.transferFrom(msg.sender, address(this), token_id);
}
function removeListing(uint listing_id) public
{
require(listings[listing_id].owner == msg.sender, "Must be owner");
require(listings[listing_id].is_active, "Must be active");
listings[listing_id].is_active = false;
token_contract.transferFrom(address(this), msg.sender, listings[listing_id].token_id);
}
function buy(uint listing_id) public payable
{
require(listings[listing_id].is_active, "Must be active");
require(listings[listing_id].price == msg.value, "Must pay the price");
listings[listing_id].is_active = false;
token_contract.transferFrom(address(this), msg.sender, listings[listing_id].token_id);
(bool sent, bytes memory data) = address(listings[listing_id].owner).call{value: msg.value}("");
data;
require(sent, "Failed to send Ether");
}
function getActiveListings(uint index) public view returns(uint)
{
uint j;
for(uint i=0; i<listing_count; i++)
{
if(listings[i].is_active)
{
if(index == j)
{
return i;
}
j+=1;
}
}
return 0;
}
function getListingsByOwner(address owner, uint index) public view returns(uint)
{
uint j;
for(uint i=0; i<listing_count; i++)
{
if(listings[i].is_active && listings[i].owner == owner)
{
if(index == j)
{
return i;
}
j+=1;
}
}
return 0;
}
function getListingsByOwnerCount(address owner) public view returns(uint)
{
uint result;
for(uint i=0; i<listing_count; i++)
{
if(listings[i].is_active && listings[i].owner == owner)
{
result+=1;
}
}
return result;
}
function getActiveListingsCount() public view returns(uint)
{
uint result;
for(uint i=0; i<listing_count; i++)
{
if(listings[i].is_active)
{
result+=1;
}
}
return result;
}
}
2. El frontend
Crea dos archivos tipo JSON ABI en una carpeta y llámalos NFTContract.json
y MarketplaceContract.json
. Luego crea los siguientes 2 archivos
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Ejemplo</title>
</head>
<body>
<h1>Marketplace</h1>
<h2>Primary Market</h2>
<button onclick="mint()" class="button is-primary">mint!</button><br>
<p id="web3_message"></p>
<h3>My NFTs</h3>
<div id="my_nfts"></div>
<h2>Secondary Market</h2>
<h3>My listings</h3>
<div id="my_listings"></div>
<h3>All listings</h3>
<div id="all_listings"></div>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/web3/1.3.5/web3.min.js"></script>
<script type="text/javascript" src="blockchain_stuff.js"></script>
</body>
</html>
En el siguiente archivo, recuerda establecer las variables TOKEN_CONTRACT_ADDRESS
y MARKETPLACE_CONTRACT_ADDRESS
con los addresses de los contratos anteriores.
blockchain_stuff.js
const NETWORK_ID = 4
const TOKEN_CONTRACT_ADDRESS = "0x0000000000000000000000000000000000000000"
const MARKETPLACE_CONTRACT_ADDRESS = "0x0000000000000000000000000000000000000000"
const TOKEN_CONTRACT_JSON_PATH = "./NFTContract.json"
const MARKETPLACE_CONTRACT_JSON_PATH = "./MarketplaceContract.json"
var token_contract
var marketplace_contract
var accounts
var web3
var balance
function metamaskReloadCallback()
{
window.ethereum.on('accountsChanged', (accounts) => {
document.getElementById("web3_message").textContent="Accounts changed, refreshing...";
window.location.reload()
})
window.ethereum.on('networkChanged', (accounts) => {
document.getElementById("web3_message").textContent="Network changed, refreshing...";
window.location.reload()
})
}
const getWeb3 = async () => {
return new Promise((resolve, reject) => {
if(document.readyState=="complete")
{
if (window.ethereum) {
const web3 = new Web3(window.ethereum)
window.location.reload()
resolve(web3)
} else {
reject("must install MetaMask")
document.getElementById("web3_message").textContent="Error: Please connect to Metamask";
}
}else
{
window.addEventListener("load", async () => {
if (window.ethereum) {
const web3 = new Web3(window.ethereum)
resolve(web3)
} else {
reject("must install MetaMask")
document.getElementById("web3_message").textContent="Error: Please install Metamask";
}
});
}
});
};
const getContract = async (web3, contract_json_path, contract_address) => {
const response = await fetch(contract_json_path);
const data = await response.json();
const netId = await web3.eth.net.getId();
contract = new web3.eth.Contract(
data,
contract_address
);
return contract
}
async function loadDapp() {
metamaskReloadCallback()
document.getElementById("web3_message").textContent="Cargando..."
var awaitWeb3 = async function () {
web3 = await getWeb3()
web3.eth.net.getId((err, netId) => {
if (netId == NETWORK_ID) {
var awaitContract = async function () {
token_contract = await getContract(web3, TOKEN_CONTRACT_JSON_PATH, TOKEN_CONTRACT_ADDRESS)
marketplace_contract = await getContract(web3, MARKETPLACE_CONTRACT_JSON_PATH, MARKETPLACE_CONTRACT_ADDRESS)
await window.ethereum.request({ method: "eth_requestAccounts" })
accounts = await web3.eth.getAccounts()
balance = await token_contract.methods.balanceOf(accounts[0]).call()
for(i=0; i<balance; i++)
{
nft_id = await token_contract.methods.tokenOfOwnerByIndex(accounts[0],i).call()
insertMyTokenHTML(nft_id)
}
my_listings_count = await marketplace_contract.methods.getListingsByOwnerCount(accounts[0]).call()
for(i=0; i<my_listings_count; i++)
{
listing_id = await marketplace_contract.methods.getListingsByOwner(accounts[0], i).call()
insertMyListingHTML(listing_id)
}
active_listing_count = await marketplace_contract.methods.getActiveListingsCount().call()
for(i=0; i<active_listing_count; i++)
{
listing_id = await marketplace_contract.methods.getActiveListings(i).call()
insertActiveListingHTML(listing_id)
}
if(balance == 1)
document.getElementById("web3_message").textContent="You have 1 token"
else
document.getElementById("web3_message").textContent="You have " + balance + " tokens"
};
awaitContract();
} else {
document.getElementById("web3_message").textContent="Please connect to Rinkeby";
}
});
};
awaitWeb3();
}
function insertMyTokenHTML(nft_id)
{
//Token number text
var token_element = document.createElement("p")
token_element.innerHTML = "Token #" + nft_id
document.getElementById("my_nfts").appendChild(token_element)
//Approve Button
let approve_btn = document.createElement("button")
approve_btn.innerHTML = "Approve"
document.getElementById("my_nfts").appendChild(approve_btn)
approve_btn.onclick = function () {
approve(MARKETPLACE_CONTRACT_ADDRESS, nft_id)
}
//Price
var input = document.createElement("input")
input.type = "text"
input.value = "Price"
input.id = "price" + nft_id
document.getElementById("my_nfts").appendChild(input)
//Sell Button
let mint_btn = document.createElement("button")
mint_btn.innerHTML = "Sell"
document.getElementById("my_nfts").appendChild(mint_btn)
mint_btn.onclick = function () {
price = document.getElementById("price" + nft_id).value;
addListing(nft_id, web3.utils.toWei(price))
}
}
async function insertMyListingHTML(listing_id)
{
listing = await marketplace_contract.methods.listings(listing_id).call()
//Token number text
var token_element = document.createElement("p")
token_element.innerHTML = "Token #" + listing.token_id + " (price: "+ web3.utils.fromWei(listing.price) +")"
document.getElementById("my_listings").appendChild(token_element)
//Delist Button
let delist_btn = document.createElement("button")
delist_btn.innerHTML = "Delist"
document.getElementById("my_listings").appendChild(delist_btn)
delist_btn.onclick = function () {
removeListing(listing_id)
}
}
async function insertActiveListingHTML(listing_id)
{
listing = await marketplace_contract.methods.listings(listing_id).call()
//Token number text
var token_element = document.createElement("p")
token_element.innerHTML = "Token #" + listing.token_id + " (price: "+ web3.utils.fromWei(listing.price) +")"
document.getElementById("all_listings").appendChild(token_element)
//Delist Button
let delist_btn = document.createElement("button")
delist_btn.innerHTML = "Buy"
document.getElementById("all_listings").appendChild(delist_btn)
delist_btn.onclick = function () {
buy(listing_id, listing.price)
}
}
const mint = async () => {
const result = await token_contract.methods.mint()
.send({ from: accounts[0], gas: 0 })
.on('transactionHash', function(hash){
document.getElementById("web3_message").textContent="Minting...";
})
.on('receipt', function(receipt){
document.getElementById("web3_message").textContent="Success!"; })
.catch((revertReason) => {
console.log("ERROR! Transaction reverted: " + revertReason.receipt.transactionHash)
});
}
const approve = async (contract_address, token_id) => {
const result = await token_contract.methods.approve(contract_address, token_id)
.send({ from: accounts[0], gas: 0 })
.on('transactionHash', function(hash){
document.getElementById("web3_message").textContent="Approving...";
})
.on('receipt', function(receipt){
document.getElementById("web3_message").textContent="Success!"; })
.catch((revertReason) => {
console.log("ERROR! Transaction reverted: " + revertReason.receipt.transactionHash)
});
}
const addListing = async (token_id, price) => {
const result = await marketplace_contract.methods.addListing(token_id, price)
.send({ from: accounts[0], gas: 0 })
.on('transactionHash', function(hash){
document.getElementById("web3_message").textContent="Adding listing...";
})
.on('receipt', function(receipt){
document.getElementById("web3_message").textContent="Success!"; })
.catch((revertReason) => {
console.log("ERROR! Transaction reverted: " + revertReason.receipt.transactionHash)
});
}
const removeListing = async (listing_id) => {
const result = await marketplace_contract.methods.removeListing(listing_id)
.send({ from: accounts[0], gas: 0 })
.on('transactionHash', function(hash){
document.getElementById("web3_message").textContent="Removing from listings...";
})
.on('receipt', function(receipt){
document.getElementById("web3_message").textContent="Success!"; })
.catch((revertReason) => {
console.log("ERROR! Transaction reverted: " + revertReason.receipt.transactionHash)
});
}
const buy = async (listing_id, price) => {
const result = await marketplace_contract.methods.buy(listing_id)
.send({ from: accounts[0], gas: 0, value: price })
.on('transactionHash', function(hash){
document.getElementById("web3_message").textContent="Buying...";
})
.on('receipt', function(receipt){
document.getElementById("web3_message").textContent="Success!"; })
.catch((revertReason) => {
console.log("ERROR! Transaction reverted: " + revertReason.receipt.transactionHash)
});
}
loadDapp()
3. Levanta el cliente
Para poder porbarlo debemos lanzar la web del cliente. Para eso ingresamos en la carpeta del cliente e instalamos la única dependencia de manera global.
npm i -g lite-server
Una vez instalada, levantamos el servidor local.
lite-server
Bono: Acepta pagos en tu ERC20
Observa cómo combinamos el estándar ERC20 y ERC721 para comprar un NFT a través de nuestro token. Otro punto a destacar que en este caso usamos el ReentrancyGuard
para mayaor protección en caso de confiar en los contratos con los que interactuamos.
Nota: Este ejemplo no es compatible con el frontend previamente desarrollado.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.11;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
struct Listing
{
address owner;
bool is_active;
uint token_id;
uint price;
}
contract Marketplace is ReentrancyGuard {
using SafeMath for uint256;
uint public listing_count = 0;
mapping (uint => Listing) public listings;
ERC721 erc721_contract = ERC721(0x0000000000000000000000000000000000000000);
ERC20 erc20_contract = ERC20(0x0000000000000000000000000000000000000000);
function addListing(uint token_id, uint price) public nonReentrant
{
listings[listing_count] = Listing(msg.sender, true, token_id, price);
listing_count = listing_count.add(1);
erc721_contract.transferFrom(msg.sender, address(this), token_id);
}
function removeListing(uint listing_id) public nonReentrant
{
require(listings[listing_id].owner == msg.sender, "Must be owner");
require(listings[listing_id].is_active, "Must be active");
listings[listing_id].is_active = false;
erc721_contract.transferFrom(address(this), msg.sender, listings[listing_id].token_id);
}
function buy(uint listing_id) public nonReentrant
{
require(listings[listing_id].is_active, "Must be active");
listings[listing_id].is_active = false;
erc20_contract.transferFrom(msg.sender, listings[listing_id].owner, listings[listing_id].price);
erc721_contract.transferFrom(address(this), msg.sender, listings[listing_id].token_id);
}
function getActiveListings(uint index) public view returns(uint)
{
uint j;
for(uint i=0; i<listing_count; i++)
{
if(listings[i].is_active)
{
if(index == j)
{
return i;
}
j+=1;
}
}
return 0;
}
function getListingsByOwner(address owner, uint index) public view returns(uint)
{
uint j;
for(uint i=0; i<listing_count; i++)
{
if(listings[i].is_active && listings[i].owner == owner)
{
if(index == j)
{
return i;
}
j+=1;
}
}
return 0;
}
function getListingsByOwnerCount(address owner) public view returns(uint)
{
uint result;
for(uint i=0; i<listing_count; i++)
{
if(listings[i].is_active && listings[i].owner == owner)
{
result+=1;
}
}
return result;
}
function getActiveListingsCount() public view returns(uint)
{
uint result;
for(uint i=0; i<listing_count; i++)
{
if(listings[i].is_active)
{
result+=1;
}
}
return result;
}
}
cd client
npm i -g lite-server
Una vez hecho esto lanzamos el juego.
lite-server
¡Gracias por ver este tutorial!
Sígueme en dev.to y en Youtube para todo lo relacionado al desarrollo en Blockchain en Español.
Top comments (9)
Hola, que tal!! Estoy tratando de desarrollar una Dapp diria sencilla, pero con un protocolo interesante, resulta que me vino bien este proyecto para experimentar, al punto que lo implemente en la Main network, con mi propio token erc20, el problema es que al momento de querer comprar un NTF, me lo manda a pagar en ETH en lugar de mi token. Salvo eso todo lo demas anda genial. Saludos!!
Hola, voy a apuntar esto para hacer un futuro video.
You are the best!!
Otra pregunta es si se puede mintear un NFT que tenga ademas de una foto, un smart contract que ejecute la funcion establecida en su creación. Ya con eso podria salvar al mundo...Gracias!!
¿Qué función quisieras ejecutar en la creación? A nivel de smartcontract puedes agregar el código que tu quieras cuando se mintea el token.
Hola hermano! gracias por responder, la cosa es asi:
“Hormiga Dapp” crea un NFT llamado “Remito Hormiga” este contiene un smart cotract que ejecuta dos transferencias al momento de mintearse:
X 1= “Valor declarado” Transfer to address= FEV (Fondos minteador congelados)
X 2= “Recompensa” Transfer to address= FER(Fondos de minteador congelados )
Importante!!:Estas dos piscinas de tokens no tienen dueño, las address se usan para congelar los tokens para el protocolo de seguridad y obtener metadata, que le dará estabilidad cambiaria al token “HTC”.
Propiedades del NFT “Remito Hormiga”
X o = Precio del NTF =(X1+X2)
X 1 = Valor declarado.
X 2 =Valor de la recompensa.
X 3 =Tiempo límite en el que debe llegar a su destinatario final.
X 4 = Origen.
X 5 = Destino.
X 6 = Address receptor final.(La unica que puede ejecutar la desmonetizacion del token ERC721)
Si el NFT llega al Address del receptor establecido por el minteador, se habilita la ejecución del smart contract por parte del receptor y se realizan las siguientes transferencias a quien sería el último address tenedor, quien entrega el item al receptor:
Transfer X 1= from FEV to Address=ultimo Tenedor (El valor declarado)
Transfer X 2= from FER to Address=ultimo Tenedor (La recompensa)
Si el NFT no llega al Address del receptor en el tiempo establecido(X 3)
El Smart contract habilita la ejecución del colateral por parte del minteador:
Transfer X 1= from FEV to Address minteador
Transfer X 2= from FER to Address minteador
Entonces el remitente emisor habrá perdido el producto pero recuperado el valor del mismo al haber vendido el NFT en el Marketplace. Sin embargo, el NFT sigue estando en poder del tenedor, por lo que si resuelve de manera humana el problema, puede concretar la operación.
Justo en este momento estamos viendo el trabajo que nos realizo un programador ruso, pero si crees que puedes ayudarnos, tenemos fundamentos para pensar que este proyecto es quizas mas importante que Bitcoin.
En términos generales para mi el proyecto tiene bastante sentido, tienes que probar para funcionar, pero yo tengo mi agenda llena, espero que te funcione.
bro segui los pasos pero a la hora de cambiar la red, me lee el login metamask pero se queda en cargando..., que error me puede estar dando alli? y pasa el tema de cobrar el nft en bnb o cualquier token de la red bsc
¿Cambiaste también el network ID? Te paso este link donde explico como hacer estos ejemplos en cualquier red. youtube.com/shorts/MhLwHPLgnF8