Professional Documents
Culture Documents
How To Build A Full Stack NFT Marketplace
How To Build A Full Stack NFT Marketplace
#webdev#react#blockchain#web3
Prerequisites
To be successful in this guide, you must have the following:
Node.js version 16.14.0 or greater installed on your machine. I recommend
installing Node using either nvm or fnm.
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.
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
Next, change into the new directory and install the dependencies using a package
manager like npm, yarn, or pnpm:
cd nft-marketplace
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
Configuring Hardhat
Next, initialize a new Hardhat development environment from the root of your
project:
npx hardhat
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
}
}
}
}
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";
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
);
_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
);
/* resell a token */
await nftMarketplace.connect(buyerAddress).resellToken(1, auctionPrice, { value:
listingPrice })
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'
/*
* 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>
)
}
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:
The user is able to upload and save files to IPFS
The user is able to create a new NFT
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'
import {
marketplaceAddress
} from '../config'
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>
)
}
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'
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>
)
}
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'
useEffect(() => {
fetchNFT()
}, [id])
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>
)
}
fs.writeFileSync('./config.js', `
export const marketplaceAddress = "${nftMarketplace.address}"
`)
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
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
When the deployment is complete, the CLI should print out the address of the
contract that was deployed:
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.
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.
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"
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.
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")
You should now be able to update the contract addresses in your project and test on
the new network 🎉!
npm run dev
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 */
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}`
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.