DEV Community

Cover image for How to Build a Full Stack NFT Marketplace - V2 (2022)
Nader Dabit for Edge and Node

Posted on • Updated on

How to Build a Full Stack NFT Marketplace - V2 (2022)

Building an NFT marketplace with Polygon, Next.js, Tailwind, Solidity, Hardhat, Ethers.js, and IPFS

To view the video course for this tutorial, click here. To view Version 1 of this tutorial, click here

In my last end to end Ethereum tutorial, The Complete Guide to Full Stack Ethereum Development I introduced how to build a basic app on Ethereum using modern tooling like Hardhat and Ethers.js.

In this guide you'll learn how to build, deploy, and test out a full stack NFT marketplace on Ethereum. We'll also look at how to deploy to Polygon.

One thing that has become apparent is how quickly Ethereum scaling solutions like Polygon, Arbitrum, and Optimism are gaining momentum and adoption. These technologies enable developers to build the same applications they would directly on Ethereum with the added benefits of lower gas costs and faster transaction speeds among other things.

Because of the value proposition that these solutions offer combined with the general lack of existing content, I will be building out various example projects and tutorials for full stack applications using these various Ethereum scaling solutions, starting with this one on Polygon.

To view the final source code for this project, visit this repo

Prerequisites

To be successful in this guide, you must have the following:

  1. Node.js version 16.14.0 or greater installed on your machine. I recommend installing Node using either nvm or fnm.
  2. Metamask wallet extension installed as a browser extension

The stack

In this guide, we will build out a full stack application using:

Web application framework - Next.js
Solidity development environment - Hardhat
File Storage - IPFS
Ethereum Web Client Library - Ethers.js

Though it will not be part of this guide (coming in a separate post), we will look at how to build a more robust API layer using The Graph Protocol to get around limitations in the data access patterns provided by the native blockchain layer.

About the project

The project that we will be building will be Metaverse Marketplace - an NFT marketplace.

Metaverse Marketplace

When a user puts an NFT for sale, the ownership of the item will be transferred from the creator to the marketplace contract.

When a user purchases an NFT, the purchase price will be transferred from the buyer to the seller and the item will be transferred from the marketplace to the buyer.

The marketplace owner will be able to set a listing fee. This fee will be taken from the seller and transferred to the contract owner upon completion of any sale, enabling the owner of the marketplace to earn recurring revenue from any sale transacted in the marketplace.

The marketplace logic will consist of just one smart contract:

NFT Marketplace Contract - this contract allows users to mint NFTs and list them in a marketplace.

I believe this is a good project because the tools, techniques, and ideas we will be working with lay the foundation for many other types of applications on this stack – dealing with things like payments, commissions, and transfers of ownership on the contract level as well as how a client-side application would use this smart contract to build a performant and nice-looking user interface.

In addition to the smart contract, I'll also show you how to build a subgraph to make the querying of data from the smart contract more flexible and efficient. As you will see, creating views on data sets and enabling various and performant data access patterns is hard to do directly from a smart contract. The Graph makes this much easier.

About Polygon

From the docs:

"Polygon is a protocol and a framework for building and connecting Ethereum-compatible blockchain networks. Aggregating scalable solutions on Ethereum supporting a multi-chain Ethereum ecosystem."

Polygon is about 10x faster than Ethereum & yet transactions are more than 10x cheaper.

Ok cool, but what does all that mean?

To me it means that I can use the same knowledge, tools, and technologies I have been using to build apps on Ethereum to build apps that are faster and cheaper for users, providing not only a better user experience but also opening the door for many types of applications that just would not be feasible to be built directly on Ethereum.

As mentioned before, there are many other Ethereum scaling solutions such as Arbitrumand Optimism that are also in a similar space. Most of these scaling solutions have technical differences and fall into various categories like sidechains , layer 2s, and state channels.

Polygon recently rebranded from Matic so you will also see the word Matic used interchangeably when referring to various parts of their ecosystem because the name still is being used in various places, like their token and network names.

To learn more about Polygon, check out this post as well as their documentation here.

Now that we have an overview of the project and related technologies, let's start building!

Project setup

To get started, we'll create a new Next.js app. To do so, open your terminal. Create or change into a new empty directory and run the following command:

npx create-next-app nft-marketplace
Enter fullscreen mode Exit fullscreen mode

Next, change into the new directory and install the dependencies using a package manager like npm, yarn, or pnpm:

cd nft-marketplace

npm install ethers hardhat @nomiclabs/hardhat-waffle \
ethereum-waffle chai @nomiclabs/hardhat-ethers \
web3modal @openzeppelin/contracts ipfs-http-client \
axios
Enter fullscreen mode Exit fullscreen mode

Setting up Tailwind CSS

We'll be using Tailwind CSS for styling, we we will set that up in this step.

Tailwind is a utility-first CSS framework that makes it easy to add styling and create good looking websites without a lot of work.

Next, install the Tailwind dependencies:

npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
Enter fullscreen mode Exit fullscreen mode

Next, we will create the configuration files needed for Tailwind to work with Next.js (tailwind.config.js and postcss.config.js) by running the following command:

npx tailwindcss init -p
Enter fullscreen mode Exit fullscreen mode

Next, configure your template content paths in tailwind.config.js:

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

Finally, delete the code in styles/globals.css and update it with the following:

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

Configuring Hardhat

Next, initialize a new Hardhat development environment from the root of your project:

npx hardhat

? What do you want to do? Create a basic sample project
? Hardhat project root: <Choose default path>
Enter fullscreen mode Exit fullscreen mode

If you get an error referencing your README.md, delete README.md and run npx hardhat again.

Now you should see the following files and folders created for you in your root directory:

hardhat.config.js - The entirety of your Hardhat setup (i.e. your config, plugins, and custom tasks) is contained in this file.
scripts - A folder containing a script named sample-script.js that will deploy your smart contract when executed
test - A folder containing an example testing script
contracts - A folder holding an example Solidity smart contract

Next, update the configuration at hardhat.config.js with the following:

View the gist here

/* hardhat.config.js */
require("@nomiclabs/hardhat-waffle")

module.exports = {
  defaultNetwork: "hardhat",
  networks: {
    hardhat: {
      chainId: 1337
    },
//  unused configuration commented out for now
//  mumbai: {
//    url: "https://rpc-mumbai.maticvigil.com",
//    accounts: [process.env.privateKey]
//  }
  },
  solidity: {
    version: "0.8.4",
    settings: {
      optimizer: {
        enabled: true,
        runs: 200
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

In this configuration, we've configured the local Hardhat development environment as well as the Mumbai testnet (commented out for now).

You can read more about both Matic networks here.

Smart Contract

Next, we'll create our smart contract!

In this file I'll do my best to comment within the code everything that is going on.

Create a new file in the contracts directory named NFTMarketplace.sol. Here, add the following code:

View the gist here

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

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 NFTMarketplace is ERC721URIStorage {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;
    Counters.Counter private _itemsSold;

    uint256 listingPrice = 0.025 ether;
    address payable owner;

    mapping(uint256 => MarketItem) private idToMarketItem;

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

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

    constructor() ERC721("Metaverse Tokens", "METT") {
      owner = payable(msg.sender);
    }

    /* Updates the listing price of the contract */
    function updateListingPrice(uint _listingPrice) public payable {
      require(owner == msg.sender, "Only marketplace owner can update listing price.");
      listingPrice = _listingPrice;
    }

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

    /* Mints a token and lists it in the marketplace */
    function createToken(string memory tokenURI, uint256 price) public payable returns (uint) {
      _tokenIds.increment();
      uint256 newTokenId = _tokenIds.current();

      _mint(msg.sender, newTokenId);
      _setTokenURI(newTokenId, tokenURI);
      createMarketItem(newTokenId, price);
      return newTokenId;
    }

    function createMarketItem(
      uint256 tokenId,
      uint256 price
    ) private {
      require(price > 0, "Price must be at least 1 wei");
      require(msg.value == listingPrice, "Price must be equal to listing price");

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

      _transfer(msg.sender, address(this), tokenId);
      emit MarketItemCreated(
        tokenId,
        msg.sender,
        address(this),
        price,
        false
      );
    }

    /* allows someone to resell a token they have purchased */
    function resellToken(uint256 tokenId, uint256 price) public payable {
      require(idToMarketItem[tokenId].owner == msg.sender, "Only item owner can perform this operation");
      require(msg.value == listingPrice, "Price must be equal to listing price");
      idToMarketItem[tokenId].sold = false;
      idToMarketItem[tokenId].price = price;
      idToMarketItem[tokenId].seller = payable(msg.sender);
      idToMarketItem[tokenId].owner = payable(address(this));
      _itemsSold.decrement();

      _transfer(msg.sender, address(this), tokenId);
    }

    /* Creates the sale of a marketplace item */
    /* Transfers ownership of the item, as well as funds between parties */
    function createMarketSale(
      uint256 tokenId
      ) public payable {
      uint price = idToMarketItem[tokenId].price;
      address seller = idToMarketItem[tokenId].seller;
      require(msg.value == price, "Please submit the asking price in order to complete the purchase");
      idToMarketItem[tokenId].owner = payable(msg.sender);
      idToMarketItem[tokenId].sold = true;
      idToMarketItem[tokenId].seller = payable(address(0));
      _itemsSold.increment();
      _transfer(address(this), msg.sender, tokenId);
      payable(owner).transfer(listingPrice);
      payable(seller).transfer(msg.value);
    }

    /* Returns all unsold market items */
    function fetchMarketItems() public view returns (MarketItem[] memory) {
      uint itemCount = _tokenIds.current();
      uint unsoldItemCount = _tokenIds.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(this)) {
          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 = _tokenIds.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 listed */
    function fetchItemsListed() public view returns (MarketItem[] memory) {
      uint totalItemCount = _tokenIds.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;
    }
}
Enter fullscreen mode Exit fullscreen mode

In this contract we are inheriting from the ERC721 standard implemented by OpenZepplin

Now the smart contract code and environment is complete and we can try testing it out.

To do so, we can create a local test to run through much of the functionality, like minting a token, putting it up for sale, selling it to a user, and querying for tokens.

To create the test, open test/sample-test.js and update it with the following code:

View the gist here

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

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

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

    /* create two tokens */
    await nftMarketplace.createToken("https://www.mytokenlocation.com", auctionPrice, { value: listingPrice })
    await nftMarketplace.createToken("https://www.mytokenlocation2.com", auctionPrice, { value: listingPrice })

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

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

    /* resell a token */
    await nftMarketplace.connect(buyerAddress).resellToken(1, auctionPrice, { value: listingPrice })

    /* query for and return the unsold items */
    items = await nftMarketplace.fetchMarketItems()
    items = await Promise.all(items.map(async i => {
      const tokenUri = await nftMarketplace.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)
  })
})
Enter fullscreen mode Exit fullscreen mode

Next, run the test from your command line:

npx hardhat test
Enter fullscreen mode Exit fullscreen mode

If the test runs successfully, it should log out an array containing the two marketplace items.

Running the test

Building the front end

Now that the smart contract is working and ready to go, we can start building out the UI.

The first thing we might think about is setting up a layout so that we can enable some navigation that will persist across all pages.

To set this up, open pages/_app.js and update it with the following code:

View the gist here

/* 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-nft">
            <a className="mr-6 text-pink-500">
              Sell NFT
            </a>
          </Link>
          <Link href="/my-nfts">
            <a className="mr-6 text-pink-500">
              My NFTs
            </a>
          </Link>
          <Link href="/dashboard">
            <a className="mr-6 text-pink-500">
              Dashboard
            </a>
          </Link>
        </div>
      </nav>
      <Component {...pageProps} />
    </div>
  )
}

export default MyApp
Enter fullscreen mode Exit fullscreen mode

The navigation has links for the home route as well as a page to sell an NFT, view the NFTs you have purchased, and a dashboard to see the NFTs you've listed.

Querying the contract for marketplace items

The next page we'll update is pages/index.js. This is the main entry-point of the app, and will be the view where we query for the NFTs for sale and render them to the screen.

View the gist here

/* pages/index.js */
import { ethers } from 'ethers'
import { useEffect, useState } from 'react'
import axios from 'axios'
import Web3Modal from 'web3modal'

import {
  marketplaceAddress
} from '../config'

import NFTMarketplace from '../artifacts/contracts/NFTMarketplace.sol/NFTMarketplace.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 contract = new ethers.Contract(marketplaceAddress, NFTMarketplace.abi, provider)
    const data = await contract.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 contract.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(marketplaceAddress, NFTMarketplace.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(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 font-bold text-white">{nft.price} ETH</p>
                  <button className="mt-4 w-full bg-pink-500 text-white font-bold py-2 px-12 rounded" onClick={() => buyNft(nft)}>Buy</button>
                </div>
              </div>
            ))
          }
        </div>
      </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

When the page loads, we query the smart contract for any NFTs that are still for sale and render them to the screen along with metadata about the items and a button for purchasing them.

Creating and listing NFTs

Next, let's create the page that allows users to create and list NFTs.

There are a few things happening in this page:

  1. The user is able to upload and save files to IPFS
  2. The user is able to create a new NFT
  3. The user is able to set metadata and price of item and list it for sale on the marketplace

After the user creates and lists an item, they are re-routed to the main page to view all of the items for sale.

View the gist here

/* pages/create-nft.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 {
  marketplaceAddress
} from '../config'

import NFTMarketplace from '../artifacts/contracts/NFTMarketplace.sol/NFTMarketplace.json'

export default function CreateItem() {
  const [fileUrl, setFileUrl] = useState(null)
  const [formInput, updateFormInput] = useState({ price: '', name: '', description: '' })
  const router = useRouter()

  async function onChange(e) {
    /* upload image to IPFS */
    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 uploadToIPFS() {
    const { name, description, price } = formInput
    if (!name || !description || !price || !fileUrl) return
    /* first, upload metadata 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 metadata is uploaded to IPFS, return the URL to use it in the transaction */
      return url
    } catch (error) {
      console.log('Error uploading file: ', error)
    }  
  }

  async function listNFTForSale() {
    const url = await uploadToIPFS()
    const web3Modal = new Web3Modal()
    const connection = await web3Modal.connect()
    const provider = new ethers.providers.Web3Provider(connection)
    const signer = provider.getSigner()

    /* create the NFT */
    const price = ethers.utils.parseUnits(formInput.price, 'ether')
    let contract = new ethers.Contract(marketplaceAddress, NFTMarketplace.abi, signer)
    let listingPrice = await contract.getListingPrice()
    listingPrice = listingPrice.toString()
    let transaction = await contract.createToken(url, 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={listNFTForSale} className="font-bold mt-4 bg-pink-500 text-white rounded p-4 shadow-lg">
          Create NFT
        </button>
      </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Viewing only the NFTs purchased by the user

In the NFTMarketplace.sol smart contract, we created a function named fetchMyNFTs that only returns the NFTs owned by the user.

In pages/my-nfts.js, we will use that function to fetch and render them.

This functionality is different than the query main pages/index.js page because we need to ask the user for their address and use it in the contract, so the user will have to sign the transaction for it to be able to fetch them properly.

View the gist here

/* pages/my-nfts.js */
import { ethers } from 'ethers'
import { useEffect, useState } from 'react'
import axios from 'axios'
import Web3Modal from 'web3modal'
import { useRouter } from 'next/router'

import {
  marketplaceAddress
} from '../config'

import NFTMarketplace from '../artifacts/contracts/NFTMarketplace.sol/NFTMarketplace.json'

export default function MyAssets() {
  const [nfts, setNfts] = useState([])
  const [loadingState, setLoadingState] = useState('not-loaded')
  const router = useRouter()
  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 marketplaceContract = new ethers.Contract(marketplaceAddress, NFTMarketplace.abi, signer)
    const data = await marketplaceContract.fetchMyNFTs()

    const items = await Promise.all(data.map(async i => {
      const tokenURI = await marketplaceContract.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,
        tokenURI
      }
      return item
    }))
    setNfts(items)
    setLoadingState('loaded') 
  }
  function listNFT(nft) {
    router.push(`/resell-nft?id=${nft.tokenId}&tokenURI=${nft.tokenURI}`)
  }
  if (loadingState === 'loaded' && !nfts.length) return (<h1 className="py-10 px-20 text-3xl">No NFTs 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>
                  <button className="mt-4 w-full bg-pink-500 text-white font-bold py-2 px-12 rounded" onClick={() => listNFT(nft)}>List</button>
                </div>
              </div>
            ))
          }
        </div>
      </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Dashboard

The next page we will be creating is the dashboard that will allow users to view all of the items they have listed.

This page will be using the fetchItemsListed function from the NFTMarketplace.sol smart contract which returns only the items that match the address of the user making the function call.

Create a new file called dashboard.js in the pages directory with the following code:

View the gist here

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

import {
  marketplaceAddress
} from '../config'

import NFTMarketplace from '../artifacts/contracts/NFTMarketplace.sol/NFTMarketplace.json'

export default function CreatorDashboard() {
  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 contract = new ethers.Contract(marketplaceAddress, NFTMarketplace.abi, signer)
    const data = await contract.fetchItemsListed()

    const items = await Promise.all(data.map(async i => {
      const tokenUri = await contract.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 NFTs listed</h1>)
  return (
    <div>
      <div className="p-4">
        <h2 className="text-2xl py-2">Items Listed</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>
  )
}
Enter fullscreen mode Exit fullscreen mode

Reselling a token

The final page we will be creating will allow users to resell an NFT they've purchased from someone else.

This page will be using the resellToken function from the NFTMarketplace.sol smart contract.

View the gist here

/* pages/resell-nft.js */
import { useEffect, useState } from 'react'
import { ethers } from 'ethers'
import { useRouter } from 'next/router'
import axios from 'axios'
import Web3Modal from 'web3modal'

import {
  marketplaceAddress
} from '../config'

import NFTMarketplace from '../artifacts/contracts/NFTMarketplace.sol/NFTMarketplace.json'

export default function ResellNFT() {
  const [formInput, updateFormInput] = useState({ price: '', image: '' })
  const router = useRouter()
  const { id, tokenURI } = router.query
  const { image, price } = formInput

  useEffect(() => {
    fetchNFT()
  }, [id])

  async function fetchNFT() {
    if (!tokenURI) return
    const meta = await axios.get(tokenURI)
    updateFormInput(state => ({ ...state, image: meta.data.image }))
  }

  async function listNFTForSale() {
    if (!price) return
    const web3Modal = new Web3Modal()
    const connection = await web3Modal.connect()
    const provider = new ethers.providers.Web3Provider(connection)
    const signer = provider.getSigner()

    const priceFormatted = ethers.utils.parseUnits(formInput.price, 'ether')
    let contract = new ethers.Contract(marketplaceAddress, NFTMarketplace.abi, signer)
    let listingPrice = await contract.getListingPrice()

    listingPrice = listingPrice.toString()
    let transaction = await contract.resellToken(id, priceFormatted, { 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 Price in Eth"
          className="mt-2 border rounded p-4"
          onChange={e => updateFormInput({ ...formInput, price: e.target.value })}
        />
        {
          image && (
            <img className="rounded mt-4" width="350" src={image} />
          )
        }
        <button onClick={listNFTForSale} className="font-bold mt-4 bg-pink-500 text-white rounded p-4 shadow-lg">
          List NFT
        </button>
      </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Running the project

To run the project, we will need to have a deploy script to deploy the smart contracts to the blockchain network.

Deploying the contracts to a local network

When we created the project, Hardhat created an example deployment script at scripts/sample-script.js.

To make the purpose of this script more clear, update the name of scripts/sample-script.js to scripts/deploy.js.

Next, update scripts/deploy.js with the following code:

const hre = require("hardhat");
const fs = require('fs');

async function main() {
  const NFTMarketplace = await hre.ethers.getContractFactory("NFTMarketplace");
  const nftMarketplace = await NFTMarketplace.deploy();
  await nftMarketplace.deployed();
  console.log("nftMarketplace deployed to:", nftMarketplace.address);

  fs.writeFileSync('./config.js', `
  export const marketplaceAddress = "${nftMarketplace.address}"
  `)
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });
Enter fullscreen mode Exit fullscreen mode

This script will deploy the contract to the blockchain network and create a file named config.js that will hold the address of the smart contract after it's been deployed.

We will first test this on a local network, then deploy it to the Mumbai testnet.

To spin up a local network, open your terminal and run the following command:

npx hardhat node
Enter fullscreen mode Exit fullscreen mode

This should create a local network with 20 accounts.

Hardhat node

Next, keep the node running and open a separate terminal window to deploy the contract.

In a separate window, run the following command:

npx hardhat run scripts/deploy.js --network localhost
Enter fullscreen mode Exit fullscreen mode

When the deployment is complete, the CLI should print out the address of the contract that was deployed:

Contract address

You should also see the config.js file populated with this smart contract address.

Importing accounts into MetaMask

You can import the accounts created by the node into your Metamask wallet to try out in the app.

Each of these accounts is seeded with 10000 ETH.

To import one of these accounts, first switch your MetaMask wallet network to Localhost 8545.

Localhost Network

Next, in MetaMask click on Import Account from the accounts menu:

MetaMask import account

Copy then paste one of the Private Keys logged out by the CLI and click Import. Once the account is imported, you should see some the Eth in the account:

MetaMask account

I'd suggest doing this with 2 or 3 accounts so that you have the ability to test out the various functionality between users.

Running the app

Now we can test out the app!

To start the app, run the following command in your CLI:

npm run dev
Enter fullscreen mode Exit fullscreen mode

To test everything out, try listing an item for sale, then switching to another account and purchasing it.

Deploying to Polygon

Now that we have the project up and running and tested locally, let's deploy to Polygon. We'll start by deploying to Mumbai, the Polygon test network.

The first thing we will need to do is save one of our private keys from our wallet as an environment variable.

To get the private key, you can use one of the private keys given to you by Hardhat or you can export them directly from MetaMask.

Private keys

If you are on a Mac, you can set an environment variable from the command line like so (be sure to run the deploy script from this same terminal and session):

export privateKey="your-private-key"
Enter fullscreen mode Exit fullscreen mode

Private keys are never meant to be shared publicly under any circumstance. It is advised never to hardcode a private key in a file. If you do choose to do so, be sure to use a testing wallet and to never under any circumstances push a file containing a private key to source control or expose it publicly.

Configuring the network

Next, we need to switch from the local test network to the Mumbai Testnet.

To do so, we need to create and set the network configuration.

First, open MetaMask and click on Settings.

MetaMask settings

Next, click on Networks and then Add Network:

New Network

Here, we will add the following configurations for the Mumbai test network as listed here:

Network Name: Mumbai TestNet
New RPC URL: https://rpc-mumbai.maticvigil.com
Chain ID: 80001
Currency Symbol: Matic

Save this, then you should be able to switch to and use the new network!

Finally, you will need some testnet Matic tokens in order to interact with the applications.

To get these, you can visit the Matic Faucet, inputting the address of the wallets that you would like to request the tokens.

Deploying to the Matic / Polygon network

Now that you have some Matic tokens, you can deploy to the Polygon network!

To do so, be sure that the address associated with the private key you are deploying your contract with has received some Matic tokens in order to pay the gas fees for the transaction.

Also, be sure to uncomment the mumbai configuration in hardhat.config.js:

    mumbai: {
      url: "https://rpc-mumbai.maticvigil.com",
      accounts: [process.env.privateKey]
    }
Enter fullscreen mode Exit fullscreen mode

To deploy to Matic, run the following command:

npx hardhat run scripts/deploy.js --network mumbai
Enter fullscreen mode Exit fullscreen mode

If you run a deployment error, the public RPC may be congested. In production, it's recommended to use an RPC provider like Infura, Alchemy, Quicknode, or Figment DataHub.

Once the contracts have been deployed, update the loadNFTs function call in pages/index.js to include the new RPC endpoint:

/* pages/index.js */

/* old provider */
const provider = new ethers.providers.JsonRpcProvider()

/* new provider */
const provider = new ethers.providers.JsonRpcProvider("https://rpc-mumbai.maticvigil.com")
Enter fullscreen mode Exit fullscreen mode

You should now be able to update the contract addresses in your project and test on the new network 🎉!

npm run dev
Enter fullscreen mode Exit fullscreen mode

If you run into an error, the contract address printed out to the console by hardhat could be incorrect due to a bug I've run into recently. You can get the correct contract addresses by visiting https://mumbai.polygonscan.com/ and pasting in the address from which the contracts were deployed to see the most recent transactions and getting the contract addresses from the transaction data.

Deploying to Mainnet

To deploy to the main Matic / Polygon network, you can use the same steps we set up for the Mumbai test network.

The main difference is that you'll need to use an endpoint for Matic as well as import the network into your MetaMask wallet as listed here.

An example update in your project to make this happen might look like this:

/* hardhat.config.js */

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

Public RPCs like the one listed above may have traffic or rate-limits depending on usage. You can sign up for a dedicated free RPC URL using services like Infura, MaticVigil, QuickNode, Alchemy, Chainstack, or Ankr.

For example, using something like Infura:

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

To view the final source code for this project, visit this repo

Next steps

Congratulations! You've deployed a non-trivial app to Polygon.

The coolest thing about working with solutions like Polygon is how little extra work or learning I had to do compared to building directly on Ethereum. Almost all of the APIs and tooling in these layer 2's and sidechains remain the same, making any skills transferable across various platforms like Polygon.

For the next steps, I'd suggest porting over the queries implemented in this app using The Graph. The Graph will open up many more data access patterns including things like pagination, filtering, and sorting which are necessary for any real-world application.

I will also be publishing a tutorial showing how to use Polygon with The Graph in the coming weeks.

Discussion (304)

Collapse
gooseophocles profile image
Goose Gordon

Has anyone received the following error? Ran into it while trying out the UI locally after running the deploy script for the first time - have tried to look around the web for answers with minimal success so far:

Error: call revert exception (method="fetchMarketItems()", errorArgs=null, errorName=null, errorSignature=null, reason=null, code=CALL_EXCEPTION, version=abi/5.4.0)

All of my code is the same as the source code currently, please let me know if anyone has any ideas as to why this is happening!

Collapse
binsta profile image
Binston-sukhael-cardoza

Delete artifacts folder and compile hardhat

Collapse
legendaryphoenixicarus profile image
legendaryphoenixicarus • Edited on

Hi folks, I think I found the solution.
You can find loadNFTs() function in pages/index.js. We use the JsonRpcProvider to get the provider. That's the point. You must provide RPC url of the testnet where you want to deploy NFT contracts. In my case, I targeted the bsctestnet, so I provide the RPC url of the bsctestnet to JsonRpcProvider constructor. Maybe you can keep the url in the config.js file. That's all. Cheers~

Thread Thread
theindianappguy profile image
Sanskar Tiwari

can you share code how? i am not clear

Thread Thread
legendaryphoenixicarus profile image
legendaryphoenixicarus • Edited on
// pages/index.js
export default function Home() {
    ... ... 
  async function loadNFTs() {    
    // update RPC url for the contracts deployed on testnet.
    const provider = new ethers.providers.JsonRpcProvider("https://data-seed-prebsc-1-s1.binance.org:8545")
    ... ...
  }
Enter fullscreen mode Exit fullscreen mode
Thread Thread
theindianappguy profile image
Sanskar Tiwari • Edited on

from where to get this rpc url?

Thread Thread
legendaryphoenixicarus profile image
legendaryphoenixicarus

In your metamask settings->networks.
dev-to-uploads.s3.amazonaws.com/up...

Thread Thread
theindianappguy profile image
Sanskar Tiwari

sorry not able to understand

Thread Thread
legendaryphoenixicarus profile image
legendaryphoenixicarus • Edited on

Okay, let me show another screenshot.
dev-to-uploads.s3.amazonaws.com/up...
dev-to-uploads.s3.amazonaws.com/up...
If you are trying to find RPC url of ropsten testnet, click the network name and simply copy the RPC url
dev-to-uploads.s3.amazonaws.com/up...
That's all. Hope this is helpful for you. Thanks.

Thread Thread
silviarainicorn profile image
Silvia Barros

Thank you! It has been a while since this post but I just wanted to thank you, this solved my problem when deploying it to a testnet (Rynkeby in my case!)

Thread Thread
sir_baller profile image
Sir Baller

@legendaryphoenixicarus I just created an account to thank you for the JsonRPCProvider code you posted above. I was struggling and this solved my problem. thanks so much!

Thread Thread
zkimhom profile image
Zkimhom

@Silvia Barros @Sir Baller @legendaryphoenixicarus
thanks for your suggestions. and i want to deploy it to a testnet (ropsten),after i follow these suggestions and change the code , i meet a question (also when i deploy it for mumbai) ,it likes this (Error HH8: There's one or more errors in your config file: * Invalid account: #0 for network: ropsten - Expected string, received undefined). can u help me solve the problem or share me the hardhat.config.js file? thank you.

Thread Thread
zkimhom profile image
Zkimhom
Thread Thread
legendaryphoenixicarus profile image
legendaryphoenixicarus • Edited on

@zkimhom

You should pass your wallet private key from .env file to the account property.

    ropsten: {
      url: `https://ropsten.infura.io/v3/${process.env.infuraId}`,
      accounts: [process.env.privateKey]
    },
Enter fullscreen mode Exit fullscreen mode
Thread Thread
zkimhom profile image
Zkimhom

ok i got it. thank you so much

Collapse
theindianappguy profile image
Sanskar Tiwari

"Delete artifacts folder and compile hardhat" this does not solved it for me

Collapse
will_urban profile image
Willian Urban

Was having the same problem and going crazy, my code was exactly like the tutorial, but one thing: the hardhat version.

After downgrading the hardhat version from 2.6.0 to 2.4.1 the issue has been fixed. (don't forget to delete the artifacts folder and run compile hardhat)

Collapse
yash_garg profile image
Megabyte

command please?

Thread Thread
yawnxyz profile image
Jan Z

I'm having no problems with version ^2.8.2 so I'd try that one first.

to change versions, do npm install hardhat@^2.8.2 and just replace the numbers at the end

Collapse
iidajun profile image
JI

I had the same problem.

The problem was solved by resetting the saved data by executing the following command described in the GitHub repository> README.

npx hardhat node
npx hardhat run scripts/deploy.js --network localhost
npm run dev
Enter fullscreen mode Exit fullscreen mode

Probably the error is due to some problem with the saved data.

Collapse
theindianappguy profile image
Sanskar Tiwari

but i want to deploy this to ropsten not localhost so other can interact with it

Collapse
earnestgdata profile image
Earnest Grasshopper 🏴󠁧󠁢󠁳󠁣󠁴󠁿

This post is old so this response probably won't help the original poster but it might help anyone else with this same error as I had. It's possible if you have his error that you're using the latest version of Node i.e., 17 which prefers IPv6 in it's search order. So when you're asking hardhat to perform it's node operation and it's creating a loopback of http://::1:8545 (IPv6) vs 127.0.0.1 which is the IPv4 version of the loopback. When you run the deploy script command (while using the IPv6) you'll have to use that 'hardhat' modifier for the network vs localhost or the command will fail and you'll get this error in your marketplace. To fix it you can change the DNS search order in Node 17 or use NVM to install an earlier version of Node.js. happy hunting!

Collapse
impwong profile image
Matt

I updated my localhost to ipv6 nothing change

Collapse
afozbek profile image
Abdullah Furkan Özbek • Edited on

Probably you deploy smart contract to local hardhat node and later you suspend it.
1- Make sure you installed hardhat
2- If you want to deploy on localhost, make sure run npx hardhat node in other terminal window.
3- Redeploy it either localhost node or test networks (no need to start node on local) like Mumbai

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

Collapse
yash_garg profile image
Megabyte • Edited on

I am getting this Error after createItem function (on click Create an Asset button). I have same RPC in Metamask and the JSONRpc. Please guide me through this. Tried to do with different accounts but didn't work.

dev-to-uploads.s3.amazonaws.com/up...

Collapse
latonet profile image
latonet

i'm getting this error and also

TypeError: marketContract.fetchMarketItems is not a function.

I've managed to upload to vercel and get it working, can create an NFT but nothing is displayed in dashboard or my items or marketplace front page.

I don't think the images are being sent to IPFS.

Anyone help?

Collapse
sherazmaker profile image
sherazmaker

If you deploy contract on localhost on port:8545 then make sure to connect to same port in metamask. And also use only JsonRpcProvide() instead of matic or any other provider.

Collapse
si3mshady profile image
Elliott Arnold

yes!1

Collapse
blackbeltdegen profile image
blackbeltDegen

change your chainID to 31337 in your metamask wallet if you are deploying to the localhost testnet

Collapse
latonet profile image
latonet

I've the same problem but no-one seems to be able to answer this. I can create NFTs but they don't show in marketplace to be able to buy

Collapse
yawnxyz profile image
Jan Z

This is happening because of a Mumbai error, outputting the wrong contract address. This causes the NFT contract to not approve the Marketplace's contract address to sell the NFTs. Redeploy with this script, by replacing main() in deploy.js with:

async function main() {
  const [deployer] = await hre.ethers.getSigners();

  console.log(
    "Deploying contracts with the account:",
    deployer.address
  );

  let txHash, txReceipt
  const NFTMarket = await hre.ethers.getContractFactory("NFTMarket");
  const nftMarket = await NFTMarket.deploy();
  await nftMarket.deployed();

  txHash = nftMarket.deployTransaction.hash;
  txReceipt = await ethers.provider.waitForTransaction(txHash);
  let nftMarketAddress = txReceipt.contractAddress

  console.log("nftMarket deployed to:", nftMarketAddress);

  const NFT = await hre.ethers.getContractFactory("NFT");
  const nft = await NFT.deploy(nftMarketAddress);
  await nft.deployed();


  txHash = nft.deployTransaction.hash;
  // console.log(`NFT hash: ${txHash}\nWaiting for transaction to be mined...`);
  txReceipt = await ethers.provider.waitForTransaction(txHash);
  let nftAddress = txReceipt.contractAddress

  console.log("nft deployed to:", nftAddress);
}
Enter fullscreen mode Exit fullscreen mode
Thread Thread
latonet profile image
latonet

Thanks for this, i've replied to your same post in github.

Thread Thread
jfreddy82s profile image
jfreddy82s

Thanks this work for me for nonce error on deploy multiple contracts

Collapse
yash_garg profile image
Megabyte

Same Issue. Its saying "execution reverted: ERC721: transfer caller is not owner nor approved"

Thread Thread
latonet profile image
latonet

my error message is

Error: call revert exception (method="fetchMarketItems()", errorArgs=null, errorName=null, errorSignature=null, reason=null, code=CALL_EXCEPTION, version=abi/5.4.0)

and others related to getting MarketItems.

Thread Thread
yash_garg profile image
Megabyte • Edited on

Yes for some reason the market.address (in the deploy.js file) doesnt console.log the address of your NFTMarket address but some random one (same thing for the NFT contract address). What you can do is look at the transaction created by your Mumbai wallet. Find this transactions then get the contract's addresses of these transactions and update them accordingly in your config.js file.
what you can do is redeploy on mumbai and instead of console.log address console.log(market.deployTransaction.hash) and console.log(nft.deployTransaction.hash) . This will give you the transactions hashes then go to mumbai.polygonscan.com/ and enter these transactions hashes in order to find the correct contracts addresses. Update your config.js accordingly.

dev-to-uploads.s3.amazonaws.com/up...

Thread Thread
latonet profile image
latonet

Hi, thanks for the reply and image, i've got it to work by adding the adding the package.json and package-lock.json from the repository then npm i, after npm run dev to localhost 3000 got a 'network RPC' but that went when i added the chainstack link to index.js for loadNFTs

const provider = new ethers.providers.JsonRpcProvider("matic-mumbai.chainstacklabs.com").

I can now create NFTs and these show in the dashboard for any user and after making changes above also in the marketplace, but i can't click on the Buy button, nothing happens.

Thanks

Thread Thread
yash_garg profile image
Megabyte

can you please share your git repo, I am having issues in the NFT contract.

Thread Thread
latonet profile image
latonet

Hi, i don't have a git repo for my site, i used localhost then vercel cli

Thread Thread
yash_garg profile image
Megabyte

can you share it somehow please then?

Thread Thread
latonet profile image
latonet

I've not changed the NFT contracts. They're the same as in the git repo from dabit3 here github.com/dabit3/polygon-ethereum....

I got the solution i tried from a discussion on the dabit repo github.com/dabit3/polygon-ethereum...

Thread Thread
latonet profile image
latonet

I use github and do send files to it bit for some reason they don't upload to the selected repository, i have about 23 repositories i use for various things but this website, and others, are cloned locally then edited and uploaded to vercel by CLI, i can't get them to work on netlify or heroku

Collapse
technophile04 profile image
Shiv Bhonde

you need to re-deploy the contract and change nftContract address and nftMarket address in config.js

Collapse
btandayamo profile image
Bertil Tandayamo • Edited on

I had same issue with JsonRpcProvider, so I did

//index.js
const provider = new ethers.providers.JsonRpcProvider('https://rpc-mainnet.maticvigil.com')
Enter fullscreen mode Exit fullscreen mode

Deploy again on mumbai test net, copy new contract address to config file and voila
PSD. Remember to have same url in metamask wallet, hardhat config file and index.js

Collapse
arielbk profile image
arielbk

I ran into issues with Infura, and with ipfs http server.

Infura requires you to create a billable account to use Polygon now, and you'll need to make another project on there for the IPFS part to get a project id and key specifically for IPFS.

After reading docs, I ended up going with the latest version of ipfs http server (^55.0.0 at time of writing) and doing the following:

import { create } from 'ipfs-http-client'

const projectId = process.env.NEXT_PUBLIC_INFURA_IPFS_PROJECT_ID
const projectSecret = process.env.NEXT_PUBLIC_INFURA_IPFS_PROJECT_SECRET
const projectIdAndSecret = `${projectId}:${projectSecret}`

const client = create({
  host: 'ipfs.infura.io',
  port: 5001,
  protocol: 'https',
  headers: {
    authorization: `Basic ${Buffer.from(projectIdAndSecret).toString(
      'base64'
    )}`,
  },
})
Enter fullscreen mode Exit fullscreen mode

I guess things are changing quickly! 😄

Collapse
zonezter_bas profile image
Bas

Could you please show an example text on how to get this in the code? I don´t really understand it. I think I´m having the IPFS problems as well.

Collapse
arealclimber profile image
Lumii • Edited on

Thank you @arielbk! Really helpful!

I'll supplement some details.

First, put the infura personal project id and project secret to .env and make sure to add .env to .gitignore:

INFURA_IPFS_PROJECT_ID="....."
INFURA_IPFS_PROJECT_SECRET=".....
Enter fullscreen mode Exit fullscreen mode

Second, set env in next.config.js:

const nextConfig = {
    reactStrictMode: true,
    env: {
        INFURA_IPFS_PROJECT_ID: process.env.INFURA_IPFS_PROJECT_ID,
        INFURA_IPFS_PROJECT_SECRET: process.env.INFURA_IPFS_PROJECT_SECRET,
    },
}

module.exports = nextConfig
Enter fullscreen mode Exit fullscreen mode

Third, change the code in create-item.js

The import and const:

import { create } from 'ipfs-http-client'

const projectId = process.env.INFURA_IPFS_PROJECT_ID
const projectSecret = process.env.INFURA_IPFS_PROJECT_SECRET
const projectIdAndSecret = `${projectId}:${projectSecret}`
const auth = `Basic ${Buffer.from(projectIdAndSecret).toString('base64')}`

const client = create({
    host: 'ipfs.infura.io',
    port: 5001,
    protocol: 'https',
    headers: {
        authorization: auth,
    },
})
Enter fullscreen mode Exit fullscreen mode

And in the function CreateItem():

    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}`

            client.pin.add(added.path).then((res) => {
                console.log(res)
                setFileUrl(url)
            })
        } catch (error) {
            console.log('Error uploading file: ', error)
        }
    }

    async function createItem() {
        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)
        }
    }

Enter fullscreen mode Exit fullscreen mode
Thread Thread
kingjulien profile image
Abusomwan Santos

Hey, I tried everything here but I get message: "Request failed with status code 400"

Collapse
yawnxyz profile image
Jan Z

I ended up signing up for an account at maticvigil.com which doesn't need a credit card

Collapse
will_urban profile image
Willian Urban

There is a small bug when buying an item when the tokenId and itemId do not match.

How to reproduce:

1- Go to /create-item, mint an item A with price X, but do not list it.
2- Mint an Item B with price Y, then list it.
3- Go to index and try to buy item B.
Result: "Please submit the asking price in order to complete the purchase".
Expected result:
Purchase successful.

Fix:

1- In index.js on the function loadNFTS add itemId: item.itemId.toNumber(), to the object item.
2- When calling createMarketSale the second argument should be nft.itemId.

As I've finished typing all this I've noticed that it has been fixed in the github repo github.com/dabit3/polygon-ethereum...

Well, at least now you know if you are looking for a solution here

Collapse
authenticfake profile image
authenticfake

yes correct, the same fix applied!

Collapse
will_urban profile image
Willian Urban • Edited on

If you are going though this tutorial, I've build a slightly more cooler NFT Marketplace based on this blog post that helped me land a job 6 months ago, and for that I thank Nader and several other free content providers.

Granted it was my first time playing with Next, and there are several things I would have done differently, but hey have a look, its not bad:

nft-marketplace-dusky.vercel.app/c...
github.com/UrbanWill/nft-marketplace

Collapse
fidalmathew profile image
Fidal Mathew

Thank you! Appreciate the help!!

Collapse
ssf profile image
Sione

if you have a setForSell function in the Market contract say after a user buys then decides to sell again, will you then need to transfer the token to the market address? like IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId). Because I have tried this and it is giving me error 'ERC721: transfer caller is not owner nor approved' but the owner of the token is the one making the transfer. Maybe im not doing something right. Please help

Collapse
mateusasferreira profile image
Mateus Ferreira

Hey, I posted my entire solution for this issue, if somebody is still facing it:
dev.to/mateusasferreira/how-to-cre...
Hope it helps ;)

Collapse
yawnxyz profile image
Jan Z

There's a way to change the deploy script w/o changing the contract itself. This is happening because of a Mumbai error, outputting the wrong contract address. This causes the NFT contract to not approve the Marketplace's contract address to sell the NFTs. Redeploy with this script, by replacing main() in deploy.js with:

async function main() {
  const [deployer] = await hre.ethers.getSigners();

  console.log(
    "Deploying contracts with the account:",
    deployer.address
  );

  let txHash, txReceipt
  const NFTMarket = await hre.ethers.getContractFactory("NFTMarket");
  const nftMarket = await NFTMarket.deploy();
  await nftMarket.deployed();

  txHash = nftMarket.deployTransaction.hash;
  txReceipt = await ethers.provider.waitForTransaction(txHash);
  let nftMarketAddress = txReceipt.contractAddress

  console.log("nftMarket deployed to:", nftMarketAddress);

  const NFT = await hre.ethers.getContractFactory("NFT");
  const nft = await NFT.deploy(nftMarketAddress);
  await nft.deployed();


  txHash = nft.deployTransaction.hash;
  // console.log(`NFT hash: ${txHash}\nWaiting for transaction to be mined...`);
  txReceipt = await ethers.provider.waitForTransaction(txHash);
  let nftAddress = txReceipt.contractAddress

  console.log("nft deployed to:", nftAddress);
}
Enter fullscreen mode Exit fullscreen mode
Collapse
mateusasferreira profile image
Mateus Ferreira

Hey, I'm having the same problem :( were you able to solve it since you posted this?

Collapse
ssf profile image
Sione

I have not. Still looking for a solution

Thread Thread
mateusasferreira profile image
Mateus Ferreira

Solved my issue yesterday by writing a custom function to tranfer tokens on my NFT contract. Like this:

contract NFT is ERC721URIStorage {
    (...)
    function transferToken(address from, address to, uint256 tokenId) external {
        require(ownerOf(tokenId) == from, "From address must be token owner");
        _transfer(from, to, tokenId);
    }
}
Enter fullscreen mode Exit fullscreen mode

And in my Market Contract:

import "./NFT.sol";

contract NFTMarket is ReentrancyGuard {
    (...)
    function putItemToResell(address nftContract, uint256 itemId, uint256 newPrice)
        public
        payable
        nonReentrant
        onlyItemOwner(itemId)
    {
        (...)
        uint256 tokenId = idToMarketItem[itemId].tokenId;

        NFT tokenContract = NFT(nftContract);

        tokenContract.transferToken(msg.sender, address(this), tokenId);        
    }

}
Enter fullscreen mode Exit fullscreen mode

Don't know all the implications of this solutions and if it may fail or cause security issues somehow but it solved my problem for know.

Thread Thread
ssf profile image
Sione

Thanks that solution works but I agree with the implications it may bring if any. Thank you though! Ill update you if I find another stable solution.

Thread Thread
mateusasferreira profile image
Mateus Ferreira

my pleasure! Keep me posted about the solutions you may find.

Thread Thread
javilonte profile image
Javilonte

Hi, thank you for posting this, i would like to use your function on my code but it says "onlyItemOwner(itemId)" never used, am i missing something? thanks again!

Thread Thread
mateusasferreira profile image
Mateus Ferreira

Hey there! its a modifier that you're missing. I added this to my contract so I can use it in functions to ensure only the item's owner can execute it:


    modifier onlyItemOwner(uint256 id) {
        require(
            idToMarketItem[id].owner == msg.sender,
            "Only product owner can do this operation"
        );
        _;
    }

Enter fullscreen mode Exit fullscreen mode
Thread Thread
javilonte profile image
Javilonte

thanksss

Collapse
giuseppecrj profile image
G.

Amazing article as always @dabit3

One small error I found is on sample-test.js instead of:

        tokenId: i.price.toString(),
Enter fullscreen mode Exit fullscreen mode

it should be

        tokenId: i.tokenId.toString(),
Enter fullscreen mode Exit fullscreen mode
Collapse
dabit3 profile image
Nader Dabit Author

Thank you! And thanks for the heads up, fixing this now!

Collapse
coderbang1 profile image
coderbang1 • Edited on

i can create items, but when i try to buy nothing happens.

console shows this error: MetaMask - RPC Error: Internal JSON-RPC error.
Object { code: -32603, message: "Internal JSON-RPC error.
(code: 3, message: "execution reverted: Please submit the asking price in order to complete the purchase")

and this one: Uncaught (in promise)

[Note that: its not on matic. i have deploy it to bsc testnet. ]

Collapse
will_urban profile image
Willian Urban
Collapse
x777 profile image
YD

Any solution?

Collapse
ayumirage profile image
Ayumirage • Edited on

im stuck on this error:

./pages/index.js:10:0
Module not found: Can't resolve '../artifacts/contracts/Market.sol/NFTMarket.json'
8 |
9 | import NFT from "../artifacts/contracts/NFT.sol/NFT.json";

10 | import Market from "../artifacts/contracts/Market.sol/NFTMarket.json";
11 |
12 | export default function Home() {
13 | const [nfts, setNfts] = useState([]);

Collapse
sumeetchawla profile image
Sumeet Chawla

If you are running on local, you haven't compiled or tested your code and hence the artifacts are not generated.

If you are trying to build on vercel, this will come cause again the artifacts are missing. The general practice is not to commit those. Add a compile (or test) command to your build script before building nextjs.

Collapse
barryhewitt profile image
Baz-Cloud

Hi, please could you help me with this, i've been following this course for days now and I'm totally stuck on this.

Once i type npm run dev this is what i get

BarryH@EPT-LT23 MINGW64 ~/digital-marketplace (main)
$ npm run dev

digital-marketplace@0.1.0 dev
next dev

ready - started server on 0.0.0.0:3000, url: localhost:3000
info - Using webpack 5. Reason: Enabled by default nextjs.org/docs/messages/webpack5
event - compiled successfully
event - build page: /
wait - compiling...
error - ./pages/index.js:11:0
Module not found: Can't resolve '../artifacts/contracts/Market.sol/NFTMarket.json'
9 |
10 | import NFT from '../artifacts/contracts/NFT.sol/NFT.json'

11 | import Market from '../artifacts/contracts/Market.sol/NFTMarket.json'
12 |
13 | export default function Home() {
14 | const [nfts, setNfts] = useState([])

Import trace for requested module:

nextjs.org/docs/messages/module-no...
event - build page: /next/dist/pages/_error
wait - compiling...
error - ./pages/index.js:11:0
Module not found: Can't resolve '../artifacts/contracts/Market.sol/NFTMarket.json'
9 |
10 | import NFT from '../artifacts/contracts/NFT.sol/NFT.json'

11 | import Market from '../artifacts/contracts/Market.sol/NFTMarket.json'
12 |
13 | export default function Home() {
14 | const [nfts, setNfts] = useState([])

Import trace for requested module:

nextjs.org/docs/messages/module-no...
wait - compiling...
event - compiled successfully

Collapse
vitustockholm profile image
Vitus

change => '../artifacts/contracts/Market.sol/NFTMarket.json' to ...... '../artifacts/contracts/NFTMarket.sol/NFTMarket.json'

Collapse
barryhewitt profile image
Baz-Cloud

Hi, i'm really stuck on this and have been trying to figure it out for hours, did you manage to figure it out?

Collapse
vitustockholm profile image
Vitus

hm, did You try figure out this: '' error - ./pages/index.js:11:0 '' *** index.js 11 code line

time is the master , and time could be disaster .. .
for me worked good enough to go and make tests :

' 'change => '../artifacts/contracts/Market.sol/NFTMarket.json' to ...... '../artifacts/contracts/NFTMarket.sol/NFTMarket.json' ''

Thread Thread
mendsalbert profile image
Mends Albert

After a couple of hours of digging I finally found the solution to this problem

  1. The 'artefacts' directory is included in the .gitignore file which makes the whole folder invisible during build
  2. open the gitignore file and exclude the artefacts directory
  3. Now run 'npm run build' and everything all the error will be gone
Collapse
btandayamo profile image
Bertil Tandayamo

Please elaborate

Collapse
genie_anabelle profile image
Genie Anabelle • Edited on

Hi, I'm having a runtime error in create-item.js when clicking to create the listing! Would appreciate any help, thank you!

The error is regarding: let value = event.args[2]

Collapse
ksteigerwald profile image
Kris Steigerwald • Edited on

Restart your hardhat node, Redeploy your contracts, update your config.js with new contract addresses. Set MetaMask custom nounce to 0 (I had to do this to deal w/ another error) and then update the nounce with each following transaction.

hth

Collapse
insivika profile image
Philipp A. • Edited on

Thank you this worked. I also had a misconfiguration in my metamask account. Can you please explain why I have to manually set the nonce? How could I expect an actual user to do this?

Thread Thread
yawnxyz profile image
Jan Z

if you get the "nonce too high" error, do this: medium.com/@thelasthash/solved-non...

Collapse
gullinburstilab profile image
Jose Lopez Jr