DEV Community

Cover image for Cómo crear un marketplace
Ahmed Castro
Ahmed Castro

Posted on • Edited on

Cómo crear un marketplace

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Una vez instalada, levantamos el servidor local.

lite-server
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode
cd client
npm i -g lite-server
Enter fullscreen mode Exit fullscreen mode

Una vez hecho esto lanzamos el juego.

lite-server
Enter fullscreen mode Exit fullscreen mode

¡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)

Collapse
 
priball profile image
Mariano Privitera

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!!

Collapse
 
turupawn profile image
Ahmed Castro

Hola, voy a apuntar esto para hacer un futuro video.

Collapse
 
priball profile image
Mariano Privitera

You are the best!!

Thread Thread
 
priball profile image
Mariano Privitera

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!!

Thread Thread
 
turupawn profile image
Ahmed Castro

¿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.

Thread Thread
 
priball profile image
Mariano Privitera

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.

Thread Thread
 
turupawn profile image
Ahmed Castro

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.

Collapse
 
agsezted profile image
AGSEZTED

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

Collapse
 
turupawn profile image
Ahmed Castro

¿Cambiaste también el network ID? Te paso este link donde explico como hacer estos ejemplos en cualquier red. youtube.com/shorts/MhLwHPLgnF8