Building an NFT marketplace DApp like Opensea with Solidity and Javascript/React may be an important step in your web3 development journey. Let's go.
In my previous tutorials, you may have learned:
how to write a smart contract and run it on local blockchain testnet using hardhat.
how to build a full-stack DApp which means deal with all three parts of a DApp: smart contract, web app and user's wallet.
how to unit test, deploy to public testnet, verify smart contract on block explorer Etherscan.
Now you can start writing a smart contract with complete functionalities: a marketplace for digital items. NFT items of an NFT collection are the digital items traded here.
Table of content
- Task 1: What we build and project setup
- Task 2: NFT collection smart contract
- Task 3: A web page to display NFT item
- Task 4: NFT marketplace smart contract
- Task 5: Webapp for NFTMarketplace
- Task 6: Deploy to Polygon and query using Alchemy NFT APIs
Nader Dabit wrote two versions of How to Build a Full Stack NFT Marketplace - V2 (2022). Inspired by his idea and based on his smart contract codebase, I write more code and write this tutorial for you.
You may already read my previous tutorials. If not, I suggest you read the following two as I will not explain some tactics which are already there.
Web3 Tutorial: build DApp with Hardhat, React and Ethers.js https://dev.to/yakult/a-tutorial-build-dapp-with-hardhat-react-and-ethersjs-1gmi
Web3 Tutorial: build DApp with Web3-React and SWR https://dev.to/yakult/tutorial-build-dapp-with-web3-react-and-swr-1fb0
Let's start building.
Task 1: What we build and project setup
Task 1.1: What we build - three parts
An NFT collection smart contract and a simple web page to display an NFT item. We will use on-chain SVG as the image of an NFT item. We need this sample NFT collection to work with in the marketplace contract as well as in the storefront.
An NFT marketplace smart contract in which user can list an NFT item and buy an NFT item. Seller can also delist his own NFT from the market. This marketplace contract also provides query functions for webapp to query market data. We will try to cover this smart contract with unit test as much as possible.
An NFT marketplace storefront using React/Web3-React/SWR. (To make it simple, we only build the necessary components of the storefront in a one-page webapp. For example, we will not provide UI components for sellers to list NFTs in the market in the webapp. )
The key part of this project is the marketplace smart contract (NFTMarketplace
) with data storage, core functions and query functions.
Core functions:
function createMarketItem(address nftContract,uint256 tokenId,uint256 price) payable
function deleteMarketItem(uint256 itemId) public
function createMarketSale(address nftContract,uint256 id) public payable
Query functions:
function fetchActiveItems() public view returns (MarketItem[] memory)
function fetchMyPurchasedItems() public view returns (MarketItem[] memory)
function fetchMyCreatedItems() public view returns (MarketItem[] memory)
A seller can use the smart contract to:
- approve an NFT to market contract
- create a market item with listing fee
- ...(waiting for a buyer to buy the NFT)...
- receive the price value the buyer paid
When a buyer buys in the market, the market contract facilitates the purchase process:
- a buyer buys an NFT by paying the price value
- market contract completes the purchase process:
- transfer the price value to the seller
- transfer the NFT from seller to buyer
- transfer the listing fee to the market owner
- change market item state from
Created
toRelease
GitHub repos of this tutorial:
- smart contracts (hardhat project): https://github.com/fjun99/nftmarketplace
- web app using React: https://github.com/fjun99/web3app-tutrial-using-web3react (nftmarket branch)
Although I learned a lot from Dabit's NFT marketplace tutorial, there are 3 major differences between what we will build and his:
Dabit's NFT is a traditional one which stores images on IPFS while our NFT stores SVG images on-chain (just data, not image). We use this option to make our tutorial simple as we don't need to setup a server to provide NFT tokenURI (restful json api) and deal with image storage on server or IPFS.
In the first version of Dabit's tutorial, he separated the NFT ERC721 token smart contract and the marketplace smart contract. In the second version, he chooses to build an NFT ERC721 with marketplace functionality in one smart contract. We choose to separate them here as we would like to build a general purpose NFT marketplace.
In Dabit's tutorial, when a seller lists an NFT item to marketplace, he transfers the NFT item to market contract and waits for it to be sold. As a blockchain and web3.0 user, I don't like this pattern. I would like to approve only the NFT item to the marketplace contract. And before it is sold, the item is still in my wallet. (I also would like not to use
setApprovalForAll()
to approve all the NFT items in this collection in my address to the market contract. We choose to approve NFT in a one-by-one style.)
Task 1.2: Directory and project setup
STEP 1: Make directories
We will separate our project into two sub-directories, chain
for hardhat project, and webapp
for React/Next.js project.
--nftmarket
--chain
--webapp
STEP 2: Hardhat project
In chain
sub-directory, instal hardhat
development environment and @openzeppelin/contracts
Solidity library. Then we init an empty hardhat project.
yarn init -y
yarn add hardhat
yarn add @openzeppelin/contracts
yarn hardhat
Alternatively, you can download the hardhat chain starter project from github repo. In your nftmarket
directory, run:
git clone git@github.com:fjun99/chain-tutorial-hardhat-starter.git chain
STEP 3: React/Next.js webapp project
You can download an empty webapp scaffold:
git clone https://github.com/fjun99/webapp-tutorial-scaffold.git webapp
You can also download the webapp codebase of this tutorial:
git clone git@github.com:fjun99/web3app-tutrial-using-web3react.git webapp
cd webapp
git checkout nftmarket
Task 2: NFT collection smart contract
Task 2.1: write an NFT smart contract
We write an NFT ERC721 smart contract inheriting OpenZeppelin's ERC721 implementation. We add three functionalities here:
- tokenId: auto increment tokenId starting from 1
- function
mintTo(address _to)
: everyone can call it to mint an NFT - function
tokenURI()
to implement token URI and on-chain SVG images
// contracts/BadgeToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "@openzeppelin/contracts/utils/Base64.sol";
contract BadgeToken is ERC721 {
uint256 private _currentTokenId = 0; //tokenId will start from 1
constructor(
string memory _name,
string memory _symbol
) ERC721(_name, _symbol) {
}
/**
* @dev Mints a token to an address with a tokenURI.
* @param _to address of the future owner of the token
*/
function mintTo(address _to) public {
uint256 newTokenId = _getNextTokenId();
_mint(_to, newTokenId);
_incrementTokenId();
}
/**
* @dev calculates the next token ID based on value of _currentTokenId
* @return uint256 for the next token ID
*/
function _getNextTokenId() private view returns (uint256) {
return _currentTokenId+1;
}
/**
* @dev increments the value of _currentTokenId
*/
function _incrementTokenId() private {
_currentTokenId++;
}
/**
* @dev return tokenURI, image SVG data in it.
*/
function tokenURI(uint256 tokenId) override public pure returns (string memory) {
string[3] memory parts;
parts[0] = "<svg xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='xMinYMin meet' viewBox='0 0 350 350'><style>.base { fill: white; font-family: serif; font-size: 300px; }</style><rect width='100%' height='100%' fill='brown' /><text x='100' y='260' class='base'>";
parts[1] = Strings.toString(tokenId);
parts[2] = "</text></svg>";
string memory json = Base64.encode(bytes(string(abi.encodePacked(
"{\"name\":\"Badge #",
Strings.toString(tokenId),
"\",\"description\":\"Badge NFT with on-chain SVG image.\",",
"\"image\": \"data:image/svg+xml;base64,",
// Base64.encode(bytes(output)),
Base64.encode(bytes(abi.encodePacked(parts[0], parts[1], parts[2]))),
"\"}"
))));
return string(abi.encodePacked("data:application/json;base64,", json));
}
}
We also add a deploy script scripts/deploy_BadgeToken.ts
which will deploy this NFT contract with name:BadgeToken
and symbol:BADGE
:
const token = await BadgeToken.deploy('BadgeToken','BADGE')
Task 2.2: Understand tokenURI()
Let's explain the implementation of ERC721 function tokenURI()
.
tokenURI()
is a metadata function for ERC721 standard. OpenZeppelin docs :
tokenURI(uint256 tokenId) → string
Returns the Uniform Resource Identifier (URI) for tokenId token.
Usually tokenURI()
returns a URI. You can get the resulting URI for each token by concatenating the baseURI and the tokenId.
In our tokenURI()
, we return URI as an object with base64 encoded instead:
First we construct the object. The svg image in the object is also base64 encoded.
{
"name":"Badge #1",
"description":"Badge NFT with on-chain SVG image."
"image":"data:image/svg+xml;base64,[svg base64 encoded]"
}
Then we return the object base64 encoded.
data:application/json;base64,(object base64 encoded)
Webapp can get URI by calling tokenURI(tokenId)
, and decode it to get the name, description and SVG image.
The SVG image is adapted from the LOOT project. It is very simple. It displays the tokenId in the image.
<svg xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='xMinYMin meet' viewBox='0 0 350 350'>
<style>.base { fill: white; font-family: serif; font-size: 300px; }</style>
<rect width='100%' height='100%' fill='brown' />
<text x='100' y='260' class='base'>
1
</text>
</svg>
Task 2.3: Unit test for ERC721 contract
Let's write an unit test script for this contract:
// test/BadgeToken.test.ts
import { expect } from "chai"
import { Signer } from "ethers"
import { ethers } from "hardhat"
import { BadgeToken } from "../typechain"
const base64 = require( "base-64")
const _name='BadgeToken'
const _symbol='BADGE'
describe("BadgeToken", function () {
let badge:BadgeToken
let account0:Signer,account1:Signer
beforeEach(async function () {
[account0, account1] = await ethers.getSigners()
const BadgeToken = await ethers.getContractFactory("BadgeToken")
badge = await BadgeToken.deploy(_name,_symbol)
})
it("Should have the correct name and symbol ", async function () {
expect(await badge.name()).to.equal(_name)
expect(await badge.symbol()).to.equal(_symbol)
})
it("Should tokenId start from 1 and auto increment", async function () {
const address1=await account1.getAddress()
await badge.mintTo(address1)
expect(await badge.ownerOf(1)).to.equal(address1)
await badge.mintTo(address1)
expect(await badge.ownerOf(2)).to.equal(address1)
expect(await badge.balanceOf(address1)).to.equal(2)
})
it("Should mint a token with event", async function () {
const address1=await account1.getAddress()
await expect(badge.mintTo(address1))
.to.emit(badge, 'Transfer')
.withArgs(ethers.constants.AddressZero,address1, 1)
})
it("Should mint a token with desired tokenURI (log result for inspection)", async function () {
const address1=await account1.getAddress()
await badge.mintTo(address1)
const tokenUri = await badge.tokenURI(1)
// console.log("tokenURI:")
// console.log(tokenUri)
const tokenId = 1
const data = base64.decode(tokenUri.slice(29))
const itemInfo = JSON.parse(data)
expect(itemInfo.name).to.be.equal('Badge #'+String(tokenId))
expect(itemInfo.description).to.be.equal('Badge NFT with on-chain SVG image.')
const svg = base64.decode(itemInfo.image.slice(26))
const idInSVG = svg.slice(256,-13)
expect(idInSVG).to.be.equal(String(tokenId))
// console.log("SVG image:")
// console.log(svg)
})
it("Should mint 10 token with desired tokenURI", async function () {
const address1=await account1.getAddress()
for(let i=1;i<=10;i++){
await badge.mintTo(address1)
const tokenUri = await badge.tokenURI(i)
const data = base64.decode(tokenUri.slice(29))
const itemInfo = JSON.parse(data)
expect(itemInfo.name).to.be.equal('Badge #'+String(i))
expect(itemInfo.description).to.be.equal('Badge NFT with on-chain SVG image.')
const svg = base64.decode(itemInfo.image.slice(26))
const idInSVG = svg.slice(256,-13)
expect(idInSVG).to.be.equal(String(i))
}
expect(await badge.balanceOf(address1)).to.equal(10)
})
})
Run the unit test:
yarn hardhat test test/BadgeToken.test.ts
Results:
BadgeToken
✓ Should have the correct name and symbol
✓ Should tokenId start from 1 and auto increment
✓ Should mint a token with event
✓ Should mint a token with desired tokenURI (log result for inspection) (62ms)
✓ Should mint 10 token with desired tokenURI (346ms)
5 passing (1s)
We can also print the tokenURI we get in the unit test for inspection:
tokenURI:
data:application/json;base64,eyJuYW1lIjoiQmFkZ2UgIzEiLCJkZXNjcmlwdGlvbiI6IkJhZGdlIE5GVCB3aXRoIG9uLWNoYWluIFNWRyBpbWFnZS4iLCJpbWFnZSI6ICJkYXRhOmltYWdlL3N2Zyt4bWw7YmFzZTY0LFBITjJaeUI0Yld4dWN6MG5hSFIwY0RvdkwzZDNkeTUzTXk1dmNtY3ZNakF3TUM5emRtY25JSEJ5WlhObGNuWmxRWE53WldOMFVtRjBhVzg5SjNoTmFXNVpUV2x1SUcxbFpYUW5JSFpwWlhkQ2IzZzlKekFnTUNBek5UQWdNelV3Sno0OGMzUjViR1UrTG1KaGMyVWdleUJtYVd4c09pQjNhR2wwWlRzZ1ptOXVkQzFtWVcxcGJIazZJSE5sY21sbU95Qm1iMjUwTFhOcGVtVTZJRE13TUhCNE95QjlQQzl6ZEhsc1pUNDhjbVZqZENCM2FXUjBhRDBuTVRBd0pTY2dhR1ZwWjJoMFBTY3hNREFsSnlCbWFXeHNQU2RpY205M2JpY2dMejQ4ZEdWNGRDQjRQU2N4TURBbklIazlKekkyTUNjZ1kyeGhjM005SjJKaGMyVW5QakU4TDNSbGVIUStQQzl6ZG1jKyJ9
SVG image:
<svg xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='xMinYMin meet' viewBox='0 0 350 350'><style>.base { fill: white; font-family: serif; font-size: 300px; }</style><rect width='100%' height='100%' fill='brown' /><text x='100' y='260' class='base'>1</text></svg>
Task 3: A web page to display NFT item
Task 3.1: Setup webapp project using Web3-React
& Chakra UI
We will use web3 connecting framework Web3-React
to get our job done. The web app stack is:
- React
- Next.js
- Chakra UI
- Web3-React
- Ethers.js
- SWR
The _app.tsx
is:
// src/pages/_app.tsx
import { ChakraProvider } from '@chakra-ui/react'
import type { AppProps } from 'next/app'
import { Layout } from 'components/layout'
import { Web3ReactProvider } from '@web3-react/core'
import { Web3Provider } from '@ethersproject/providers'
function getLibrary(provider: any): Web3Provider {
const library = new Web3Provider(provider)
return library
}
function MyApp({ Component, pageProps }: AppProps) {
return (
<Web3ReactProvider getLibrary={getLibrary}>
<ChakraProvider>
<Layout>
<Component {...pageProps} />
</Layout>
</ChakraProvider>
</Web3ReactProvider>
)
}
export default MyApp
We will use the ConnectMetamask
component in our previous tutorial: Tutorial: build DApp with Web3-React and SWRTutorial: build DApp with Web3-React and SWR.
Task 3.2: Write a component to display NFT time
In this component, we also use SWR
as we do in the previous tutorial. The SWR
fetcher is in utils/fetcher.tsx
.
// components/CardERC721.tsx
import React, { useEffect,useState } from 'react';
import { useWeb3React } from '@web3-react/core'
import { Web3Provider } from '@ethersproject/providers'
import { Box, Text} from '@chakra-ui/react'
import useSWR from 'swr'
import { ERC721ABI as abi} from "abi/ERC721ABI"
import { BigNumber } from 'ethers'
import { fetcher } from 'utils/fetcher'
const base64 = require( "base-64")
interface Props {
addressContract: string,
tokenId:BigNumber
}
interface ItemInfo{
name:string,
description:string,
svg:string
}
export default function CardERC721(props:Props){
const addressContract = props.addressContract
const { account, active, library } = useWeb3React<Web3Provider>()
const [itemInfo, setItemInfo] = useState<ItemInfo>()
const { data: nftURI } = useSWR([addressContract, 'tokenURI', props.tokenId], {
fetcher: fetcher(library, abi),
})
useEffect( () => {
if(!nftURI) return
const data = base64.decode(nftURI.slice(29))
const itemInfo = JSON.parse(data)
const svg = base64.decode(itemInfo.image.slice(26))
setItemInfo({
"name":itemInfo.name,
"description":itemInfo.description,
"svg":svg})
},[nftURI])
return (
<Box my={2} bg='gray.100' borderRadius='md' width={220} height={260} px={3} py={4}>
{itemInfo
?<Box>
<img src={`data:image/svg+xml;utf8,${itemInfo.svg}`} alt={itemInfo.name} width= '200px' />
<Text fontSize='xl' px={2} py={2}>{itemInfo.name}</Text>
</Box>
:<Box />
}
</Box>
)
}
Some explanations:
- When connected to MetaMask wallet, this component queries tokenURI(tokenId) to get name, description and svg image of the NFT item.
Let's write a page to display NFT item.
// src/pages/samplenft.tsx
import type { NextPage } from 'next'
import Head from 'next/head'
import { VStack, Heading } from "@chakra-ui/layout"
import ConnectMetamask from 'components/ConnectMetamask'
import CardERC721 from 'components/CardERC721'
import { BigNumber } from 'ethers'
const nftAddress = '0x5fbdb2315678afecb367f032d93f642f64180aa3'
const tokenId = BigNumber.from(1)
const SampleNFTPage: NextPage = () => {
return (
<>
<Head>
<title>My DAPP</title>
</Head>
<Heading as="h3" my={4}>NFT Marketplace</Heading>
<ConnectMetamask />
<VStack>
<CardERC721 addressContract={nftAddress} tokenId={tokenId} ></CardERC721>
</VStack>
</>
)
}
export default SampleNFTPage
Task 3.3: Run the webapp project
STEP 1: Run a stand-alone local testnet
In another terminal, run in chain/
directory:
yarn hardhat node
STEP 2: Deploy BadgeToken (ERC721) to local testnet
yarn hardhat run scripts/deploy_BadgeToken.ts --network localhost
Result:
Deploying BadgeToken ERC721 token...
BadgeToken deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
STEP 3: Mint a BadgeToken (tokenId = 1) in hardhat console
Run hardhat console connect to local testenet
yarn hardhat console --network localhost
In console:
nftaddress = '0x5FbDB2315678afecb367f032d93F642f64180aa3'
nft = await ethers.getContractAt("BadgeToken", nftaddress)
await nft.name()
//'BadgeToken'
await nft.mintTo('0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266')
// tx response ...
await nft.tokenURI(1)
//'data:application/json;base64,eyJuYW1lIjoiQmFkZ2UgIzEiLCJk...'
Now we have the NFT item. We will display it on the web page.
STEP 3: prepare your MetaMask
Make sure your MetaMask has the local testnet wich RPC URL http://localhost:8545
and chain id 31337
.
STEP 4: run webapp
In webapp/
, run:
yarn dev
In chrome browser, goto page: http://localhost:3000/samplenft
.
Connect MetaMask, the NFT item will be displayed on the page. (Please note that the image is lazy loading. Wait for loading to be completed.
We can see that the NFT "Badge #1" with tokenId 1
is displayed correctly.
Task 4: NFT marketplace smart contract
Task 4.1: Contract data structure
We adapted the Market.sol
smart contract from Nader Dabit's tutorial (V1) to write our marketplace. Thanks a lot. But you should note that we make a lot of changes in this contract.
We define a struct MarketItem
:
struct MarketItem {
uint id;
address nftContract;
uint256 tokenId;
address payable seller;
address payable buyer;
uint256 price;
State state;
}
Each market item can be in one of three states:
enum State { Created, Release, Inactive }
Please note that we can't rely on Created
State. If seller transfers the NFT item to others or seller removes approval of the NFT item, the state will still be Created
indicating others can buy in the market. Actually others can't buy it.
All items are stored in a mapping
:
mapping(uint256 => MarketItem) private marketItems;
The market has an owner who is the contract deployer. Listing fee will be going to market owner when a listed NFT item is sold in the market.
In the future, we may add functionalities to transfer the ownership to other address or multi-sig wallet. To make the tutorial simple, we skip these functionalities.
This market has a static listing fee:
uint256 public listingFee = 0.025 ether;
function getListingFee() public view returns (uint256)
Task 4.2: market methods
The marketplace has two categories of methods:
Core functions:
function createMarketItem(address nftContract,uint256 tokenId,uint256 price) payable
function deleteMarketItem(uint256 itemId) public
function createMarketSale(address nftContract,uint256 id) public payable
Query functions:
function fetchActiveItems() public view returns (MarketItem[] memory)
function fetchMyPurchasedItems() public view returns (MarketItem[] memory)
function fetchMyCreatedItems() public view returns (MarketItem[] memory)
The full smart contract goes as follows:
// contracts/NFTMarketplace.sol
// SPDX-License-Identifier: MIT OR Apache-2.0
//
// adapt and edit from (Nader Dabit):
// https://github.com/dabit3/polygon-ethereum-nextjs-marketplace/blob/main/contracts/Market.sol
pragma solidity ^0.8.3;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "hardhat/console.sol";
contract NFTMarketplace is ReentrancyGuard {
using Counters for Counters.Counter;
Counters.Counter private _itemCounter;//start from 1
Counters.Counter private _itemSoldCounter;
address payable public marketowner;
uint256 public listingFee = 0.025 ether;
enum State { Created, Release, Inactive }
struct MarketItem {
uint id;
address nftContract;
uint256 tokenId;
address payable seller;
address payable buyer;
uint256 price;
State state;
}
mapping(uint256 => MarketItem) private marketItems;
event MarketItemCreated (
uint indexed id,
address indexed nftContract,
uint256 indexed tokenId,
address seller,
address buyer,
uint256 price,
State state
);
event MarketItemSold (
uint indexed id,
address indexed nftContract,
uint256 indexed tokenId,
address seller,
address buyer,
uint256 price,
State state
);
constructor() {
marketowner = payable(msg.sender);
}
/**
* @dev Returns the listing fee of the marketplace
*/
function getListingFee() public view returns (uint256) {
return listingFee;
}
/**
* @dev create a MarketItem for NFT sale on the marketplace.
*
* List an NFT.
*/
function createMarketItem(
address nftContract,
uint256 tokenId,
uint256 price
) public payable nonReentrant {
require(price > 0, "Price must be at least 1 wei");
require(msg.value == listingFee, "Fee must be equal to listing fee");
require(IERC721(nftContract).getApproved(tokenId) == address(this), "NFT must be approved to market");
// change to approve mechanism from the original direct transfer to market
// IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId);
_itemCounter.increment();
uint256 id = _itemCounter.current();
marketItems[id] = MarketItem(
id,
nftContract,
tokenId,
payable(msg.sender),
payable(address(0)),
price,
State.Created
);
emit MarketItemCreated(
id,
nftContract,
tokenId,
msg.sender,
address(0),
price,
State.Created
);
}
/**
* @dev delete a MarketItem from the marketplace.
*
* de-List an NFT.
*
* todo ERC721.approve can't work properly!! comment out
*/
function deleteMarketItem(uint256 itemId) public nonReentrant {
require(itemId <= _itemCounter.current(), "id must <= item count");
require(marketItems[itemId].state == State.Created, "item must be on market");
MarketItem storage item = marketItems[itemId];
require(IERC721(item.nftContract).ownerOf(item.tokenId) == msg.sender, "must be the owner");
require(IERC721(item.nftContract).getApproved(item.tokenId) == address(this), "NFT must be approved to market");
item.state = State.Inactive;
emit MarketItemSold(
itemId,
item.nftContract,
item.tokenId,
item.seller,
address(0),
0,
State.Inactive
);
}
/**
* @dev (buyer) buy a MarketItem from the marketplace.
* Transfers ownership of the item, as well as funds
* NFT: seller -> buyer
* value: buyer -> seller
* listingFee: contract -> marketowner
*/
function createMarketSale(
address nftContract,
uint256 id
) public payable nonReentrant {
MarketItem storage item = marketItems[id]; //should use storge!!!!
uint price = item.price;
uint tokenId = item.tokenId;
require(msg.value == price, "Please submit the asking price");
require(IERC721(nftContract).getApproved(tokenId) == address(this), "NFT must be approved to market");
IERC721(nftContract).transferFrom(item.seller, msg.sender, tokenId);
payable(marketowner).transfer(listingFee);
item.seller.transfer(msg.value);
item.buyer = payable(msg.sender);
item.state = State.Release;
_itemSoldCounter.increment();
emit MarketItemSold(
id,
nftContract,
tokenId,
item.seller,
msg.sender,
price,
State.Release
);
}
/**
* @dev Returns all unsold market items
* condition:
* 1) state == Created
* 2) buyer = 0x0
* 3) still have approve
*/
function fetchActiveItems() public view returns (MarketItem[] memory) {
return fetchHepler(FetchOperator.ActiveItems);
}
/**
* @dev Returns only market items a user has purchased
* todo pagination
*/
function fetchMyPurchasedItems() public view returns (MarketItem[] memory) {
return fetchHepler(FetchOperator.MyPurchasedItems);
}
/**
* @dev Returns only market items a user has created
* todo pagination
*/
function fetchMyCreatedItems() public view returns (MarketItem[] memory) {
return fetchHepler(FetchOperator.MyCreatedItems);
}
enum FetchOperator { ActiveItems, MyPurchasedItems, MyCreatedItems}
/**
* @dev fetch helper
* todo pagination
*/
function fetchHepler(FetchOperator _op) private view returns (MarketItem[] memory) {
uint total = _itemCounter.current();
uint itemCount = 0;
for (uint i = 1; i <= total; i++) {
if (isCondition(marketItems[i], _op)) {
itemCount ++;
}
}
uint index = 0;
MarketItem[] memory items = new MarketItem[](itemCount);
for (uint i = 1; i <= total; i++) {
if (isCondition(marketItems[i], _op)) {
items[index] = marketItems[i];
index ++;
}
}
return items;
}
/**
* @dev helper to build condition
*
* todo should reduce duplicate contract call here
* (IERC721(item.nftContract).getApproved(item.tokenId) called in two loop
*/
function isCondition(MarketItem memory item, FetchOperator _op) private view returns (bool){
if(_op == FetchOperator.MyCreatedItems){
return
(item.seller == msg.sender
&& item.state != State.Inactive
)? true
: false;
}else if(_op == FetchOperator.MyPurchasedItems){
return
(item.buyer == msg.sender) ? true: false;
}else if(_op == FetchOperator.ActiveItems){
return
(item.buyer == address(0)
&& item.state == State.Created
&& (IERC721(item.nftContract).getApproved(item.tokenId) == address(this))
)? true
: false;
}else{
return false;
}
}
}
This NFTMarket contract can work, but it is not good. There are at least two jobs to be done:
We should add pagination in the query functions. If there are thousands of items in the market, the query function can't work well.
When we try to verify whether a seller has already transferred the NFT item to others or has removed the approval to the market, we call
nft.getApproved
thousands of times. This is bad practice. We should try to figure out a solution.
We may also find that letting the webapp query data directly from a smart contract is not a good design. A data indexing layer is needed. The Graph Protocol and subgraph can do this job. You can find an explanation on how to use subgraph in Dabit's NFT market tutorial.
Thinking Note on delegatecall
When I built the NFTMarketplace smart contract, I explored the wrong path for about one day and learned a lot. Here is what I learned.
When a seller lists an NFT to the marketplace, he gives market contract approval
approve(marketaddress)
to transfer NFT from seller to buyer by callingtransferFrom()
. I would like to choose not to usesetApprovalForAll(operator, approved)
which will give market contract approval of all my NFTs in one collection.Seller may want to delete(de-list) an NFT from the market, so we add a function
deleteMarketItem(itemId)
.-
The wrong path starts here. I am trying to remove approval for the seller in the market contract.
- Call
nft.approve(address(0),tokenId)
will revert. The market contract is not the owner of this NFT or approved for all as an operator. - Maybe we can using
delegatecall
which will be called using the originalmsg.sender
(the seller). The seller is the owner. - I always get "Error: VM Exception while processing transaction: reverted with reason string 'ERC721: owner query for nonexistent token'". What's going wrong?
- When I try to delegate call other functions such as
name()
, the result is not correct. - Dig, dig, and dig. Finally, I found that I misunderstood
delegatecall
. Delegatecall uses the storage of the caller(market contract), and it doesn't use the storage of the callee(nft contract). Solidity Docs writes: "Storage, current address and balance still refer to the calling contract, only the code is taken from the called address. " - So we can't delegate call
nft.approve()
to remove approval in market contract. We can't access the original data in the NFT contract by delegatecall.
- Call
The delegatecall code snippet (which is wrong):
bytes memory returndata = Address.functionDelegateCall(
item.nftContract,
abi.encodeWithSignature("approve(address,uint256)",address(0),1)
);
Address.verifyCallResult(true, returndata, "approve revert");
-
But this is not the end. I finally found that I should not try to remove approval in the market contract. The logic is wrong.
- Seller calls market contract
deleteMarketItem
to remove market item. - Seller doesn't ask market contract to call nft contract "approve()" to remove the approval. (There is a
ERC20Permit
, but there is no permit in ERC721 yet.) - The design of blockchain don't allow contract to do this.
- Seller calls market contract
If the seller wants to do this, he should do it by himself by calling
approve()
directly. This is what we do in the unit testawait nft.approve(ethers.constants.AddressZero,1)
Opensea suggests to use isApprovedForAll
in its tutorial (sample code):
/**
* Override isApprovedForAll to whitelist user's OpenSea proxy accounts to enable gas-less listings.
*/
function isApprovedForAll(address owner, address operator)
override
public
view
returns (bool)
{
// Whitelist OpenSea proxy contract for easy trading.
ProxyRegistry proxyRegistry = ProxyRegistry(proxyRegistryAddress);
if (address(proxyRegistry.proxies(owner)) == operator) {
return true;
}
return super.isApprovedForAll(owner, operator);
}
The "approve for all" mechanism is quite complicated and you can refer to the opensea proxy contract for more information.
Task 4.3: Unit test for NFTMarketplace (core function)
We will add two unit test scripts for NFTMarketplace:
- one for core functions
- one for query/fetch functions
Unit test script for core functions:
// NFTMarketplace.test.ts
import { expect } from "chai"
import { BigNumber, Signer } from "ethers"
import { ethers } from "hardhat"
import { BadgeToken, NFTMarketplace } from "../typechain"
import { TransactionResponse, TransactionReceipt } from "@ethersproject/providers"
const _name='BadgeToken'
const _symbol='BADGE'
describe("NFTMarketplace", function () {
let nft:BadgeToken
let market:NFTMarketplace
let account0:Signer,account1:Signer,account2:Signer
let address0:string, address1:string, address2:string
let listingFee:BigNumber
const auctionPrice = ethers.utils.parseUnits('1', 'ether')
beforeEach(async function () {
[account0, account1, account2] = await ethers.getSigners()
address0 = await account0.getAddress()
address1 = await account1.getAddress()
address2 = await account2.getAddress()
const BadgeToken = await ethers.getContractFactory("BadgeToken")
nft = await BadgeToken.deploy(_name,_symbol)
const Market = await ethers.getContractFactory("NFTMarketplace")
market = await Market.deploy()
listingFee = await market.getListingFee()
})
it("Should create market item successfully", async function() {
await nft.mintTo(address0) //tokenId=1
await nft.approve(market.address,1)
await market.createMarketItem(nft.address, 1, auctionPrice, { value: listingFee })
const items = await market.fetchMyCreatedItems()
expect(items.length).to.be.equal(1)
})
it("Should create market item with EVENT", async function() {
await nft.mintTo(address0) //tokenId=1
await nft.approve(market.address,1)
await expect(market.createMarketItem(nft.address, 1, auctionPrice, { value: listingFee }))
.to.emit(market, 'MarketItemCreated')
.withArgs(
1,
nft.address,
1,
address0,
ethers.constants.AddressZero,
auctionPrice,
0)
})
it("Should revert to create market item if nft is not approved", async function() {
await nft.mintTo(address0) //tokenId=1
// await nft.approve(market.address,1)
await expect(market.createMarketItem(nft.address, 1, auctionPrice, { value: listingFee }))
.to.be.revertedWith('NFT must be approved to market')
})
it("Should create market item and buy (by address#1) successfully", async function() {
await nft.mintTo(address0) //tokenId=1
await nft.approve(market.address,1)
await market.createMarketItem(nft.address, 1, auctionPrice, { value: listingFee })
await expect(market.connect(account1).createMarketSale(nft.address, 1, { value: auctionPrice}))
.to.emit(market, 'MarketItemSold')
.withArgs(
1,
nft.address,
1,
address0,
address1,
auctionPrice,
1)
expect(await nft.ownerOf(1)).to.be.equal(address1)
})
it("Should revert buy if seller remove approve", async function() {
await nft.mintTo(address0) //tokenId=1
await nft.approve(market.address,1)
await market.createMarketItem(nft.address, 1, auctionPrice, { value: listingFee })
await nft.approve(ethers.constants.AddressZero,1)
await expect(market.connect(account1).createMarketSale(nft.address, 1, { value: auctionPrice}))
.to.be.reverted
})
it("Should revert buy if seller transfer the token out", async function() {
await nft.mintTo(address0) //tokenId=1
await nft.approve(market.address,1)
await market.createMarketItem(nft.address, 1, auctionPrice, { value: listingFee })
await nft.transferFrom(address0,address2,1)
await expect(market.connect(account1).createMarketSale(nft.address, 1, { value: auctionPrice}))
.to.be.reverted
})
it("Should revert to delete(de-list) with wrong params", async function() {
await nft.mintTo(address0) //tokenId=1
await nft.approve(market.address,1)
await market.createMarketItem(nft.address, 1, auctionPrice, { value: listingFee })
//not a correct id
await expect(market.deleteMarketItem(2)).to.be.reverted
//not owner
await expect(market.connect(account1).deleteMarketItem(1)).to.be.reverted
await nft.transferFrom(address0,address1,1)
//not approved to market now
await expect(market.deleteMarketItem(1)).to.be.reverted
})
it("Should create market item and delete(de-list) successfully", async function() {
await nft.mintTo(address0) //tokenId=1
await nft.approve(market.address,1)
await market.createMarketItem(nft.address, 1, auctionPrice, { value: listingFee })
await market.deleteMarketItem(1)
await nft.approve(ethers.constants.AddressZero,1)
// should revert if trying to delete again
await expect(market.deleteMarketItem(1))
.to.be.reverted
})
it("Should seller, buyer and market owner correct ETH value after sale", async function() {
let txresponse:TransactionResponse, txreceipt:TransactionReceipt
let gas
const marketownerBalance = await ethers.provider.getBalance(address0)
await nft.connect(account1).mintTo(address1) //tokenId=1
await nft.connect(account1).approve(market.address,1)
let sellerBalance = await ethers.provider.getBalance(address1)
txresponse = await market.connect(account1).createMarketItem(nft.address, 1, auctionPrice, { value: listingFee })
const sellerAfter = await ethers.provider.getBalance(address1)
txreceipt = await txresponse.wait()
gas = txreceipt.gasUsed.mul(txreceipt.effectiveGasPrice)
// sellerAfter = sellerBalance - listingFee - gas
expect(sellerAfter).to.equal(sellerBalance.sub( listingFee).sub(gas))
const buyerBalance = await ethers.provider.getBalance(address2)
txresponse = await market.connect(account2).createMarketSale(nft.address, 1, { value: auctionPrice})
const buyerAfter = await ethers.provider.getBalance(address2)
txreceipt = await txresponse.wait()
gas = txreceipt.gasUsed.mul(txreceipt.effectiveGasPrice)
expect(buyerAfter).to.equal(buyerBalance.sub(auctionPrice).sub(gas))
const marketownerAfter = await ethers.provider.getBalance(address0)
expect(marketownerAfter).to.equal(marketownerBalance.add(listingFee))
})
})
Run:
yarn hardhat test test/NFTMarketplace.test.ts
Results:
NFTMarketplace
✓ Should create market item successfully (49ms)
✓ Should create market item with EVENT
✓ Should revert to create market item if nft is not approved
✓ Should create market item and buy (by address#1) successfully (48ms)
✓ Should revert buy if seller remove approve (49ms)
✓ Should revert buy if seller transfer the token out (40ms)
✓ Should revert to delete(de-list) with wrong params (49ms)
✓ Should create market item and delete(de-list) successfully (44ms)
✓ Should seller, buyer and market owner correct ETH value after sale (43ms)
9 passing (1s)
Task 4.4: Unit test for NFTMarketplace (query function)
Unit test script for query functions:
// NFTMarketplaceFetch.test.ts
import { expect } from "chai"
import { BigNumber, Signer } from "ethers"
import { ethers } from "hardhat"
import { BadgeToken, NFTMarketplace } from "../typechain"
const _name='BadgeToken'
const _symbol='BADGE'
describe("NFTMarketplace Fetch functions", function () {
let nft:BadgeToken
let market:NFTMarketplace
let account0:Signer,account1:Signer,account2:Signer
let address0:string, address1:string, address2:string
let listingFee:BigNumber
const auctionPrice = ethers.utils.parseUnits('1', 'ether')
beforeEach(async function () {
[account0, account1, account2] = await ethers.getSigners()
address0 = await account0.getAddress()
address1 = await account1.getAddress()
address2 = await account2.getAddress()
const BadgeToken = await ethers.getContractFactory("BadgeToken")
nft = await BadgeToken.deploy(_name,_symbol)
// tokenAddress = nft.address
const Market = await ethers.getContractFactory("NFTMarketplace")
market = await Market.deploy()
listingFee = await market.getListingFee()
// console.log("1. == mint 1-6 to account#0")
for(let i=1;i<=6;i++){
await nft.mintTo(address0)
}
// console.log("3. == mint 7-9 to account#1")
for(let i=7;i<=9;i++){
await nft.connect(account1).mintTo(address1)
}
// console.log("2. == list 1-6 to market")
for(let i=1;i<=6;i++){
await nft.approve(market.address,i)
await market.createMarketItem(nft.address, i, auctionPrice, { value: listingFee })
}
})
it("Should fetchActiveItems correctly", async function() {
const items = await market.fetchActiveItems()
expect(items.length).to.be.equal(6)
})
it("Should fetchMyCreatedItems correctly", async function() {
const items = await market.fetchMyCreatedItems()
expect(items.length).to.be.equal(6)
//should delete correctly
await market.deleteMarketItem(1)
const newitems = await market.fetchMyCreatedItems()
expect(newitems.length).to.be.equal(5)
})
it("Should fetchMyPurchasedItems correctly", async function() {
const items = await market.fetchMyPurchasedItems()
expect(items.length).to.be.equal(0)
})
it("Should fetchActiveItems with correct return values", async function() {
const items = await market.fetchActiveItems()
expect(items[0].id).to.be.equal(BigNumber.from(1))
expect(items[0].nftContract).to.be.equal(nft.address)
expect(items[0].tokenId).to.be.equal(BigNumber.from(1))
expect(items[0].seller).to.be.equal(address0)
expect(items[0].buyer).to.be.equal(ethers.constants.AddressZero)
expect(items[0].state).to.be.equal(0)//enum State.Created
})
it("Should fetchMyPurchasedItems with correct return values", async function() {
await market.connect(account1).createMarketSale(nft.address, 1, { value: auctionPrice})
const items = await market.connect(account1).fetchMyPurchasedItems()
expect(items[0].id).to.be.equal(BigNumber.from(1))
expect(items[0].nftContract).to.be.equal(nft.address)
expect(items[0].tokenId).to.be.equal(BigNumber.from(1))
expect(items[0].seller).to.be.equal(address0)
expect(items[0].buyer).to.be.equal(address1)//address#1
expect(items[0].state).to.be.equal(1)//enum State.Release
})
})
Run:
yarn hardhat test test/NFTMarketplaceFetch.test.ts
Results:
NFTMarketplace Fetch functions
✓ Should fetchActiveItems correctly (48ms)
✓ Should fetchMyCreatedItems correctly (54ms)
✓ Should fetchMyPurchasedItems correctly
✓ Should fetchActiveItems with correct return values
✓ Should fetchMyPurchasedItems with correct return values
5 passing (2s)
Task 4.5: playMarket.ts
helper script for developing smart contract
We write a script src/playMarket.ts
. During the development and debug process, I run this script again and again. It helps me to see whether the market contract can work as it is designed to.
// src/playMarket.ts
import { Signer } from "ethers"
import { ethers } from "hardhat"
import { BadgeToken, NFTMarketplace } from "../typechain"
const base64 = require( "base-64")
const _name='BadgeToken'
const _symbol='BADGE'
async function main() {
let account0:Signer,account1:Signer
[account0, account1] = await ethers.getSigners()
const address0=await account0.getAddress()
const address1=await account1.getAddress()
/* deploy the marketplace */
const Market = await ethers.getContractFactory("NFTMarketplace")
const market:NFTMarketplace = await Market.deploy()
await market.deployed()
const marketAddress = market.address
/* deploy the NFT contract */
const NFT = await ethers.getContractFactory("BadgeToken")
const nft:BadgeToken = await NFT.deploy(_name,_symbol)
await nft.deployed()
const tokenAddress = nft.address
console.log("marketAddress",marketAddress)
console.log("nftContractAddress",tokenAddress)
/* create two tokens */
await nft.mintTo(address0) //'1'
await nft.mintTo(address0) //'2'
await nft.mintTo(address0) //'3'
const listingFee = await market.getListingFee()
const auctionPrice = ethers.utils.parseUnits('1', 'ether')
await nft.approve(marketAddress,1)
await nft.approve(marketAddress,2)
await nft.approve(marketAddress,3)
console.log("Approve marketAddress",marketAddress)
// /* put both tokens for sale */
await market.createMarketItem(tokenAddress, 1, auctionPrice, { value: listingFee })
await market.createMarketItem(tokenAddress, 2, auctionPrice, { value: listingFee })
await market.createMarketItem(tokenAddress, 3, auctionPrice, { value: listingFee })
// test transfer
await nft.transferFrom(address0,address1,2)
/* execute sale of token to another user */
await market.connect(account1).createMarketSale(tokenAddress, 1, { value: auctionPrice})
/* query for and return the unsold items */
console.log("==after purchase & Transfer==")
let items = await market.fetchActiveItems()
let printitems
printitems = await parseItems(items,nft)
printitems.map((item)=>{printHelper(item,true,false)})
// console.log( await parseItems(items,nft))
console.log("==after delete==")
await market.deleteMarketItem(3)
items = await market.fetchActiveItems()
printitems = await parseItems(items,nft)
printitems.map((item)=>{printHelper(item,true,false)})
// console.log( await parseItems(items,nft))
console.log("==my list items==")
items = await market.fetchMyCreatedItems()
printitems = await parseItems(items,nft)
printitems.map((item)=>{printHelper(item,true,false)})
console.log("")
console.log("==address1 purchased item (only one, tokenId =1)==")
items = await market.connect(account1).fetchMyPurchasedItems()
printitems = await parseItems(items,nft)
printitems.map((item)=>{printHelper(item,true,true)})
}
async function parseItems(items:any,nft:BadgeToken) {
let parsed= await Promise.all(items.map(async (item:any) => {
const tokenUri = await nft.tokenURI(item.tokenId)
return {
price: item.price.toString(),
tokenId: item.tokenId.toString(),
seller: item.seller,
buyer: item.buyer,
tokenUri
}
}))
return parsed
}
function printHelper(item:any,flagUri=false,flagSVG=false){
if(flagUri){
const {name,description,svg}= parseNFT(item)
console.log("id & name:",item.tokenId,name)
if(flagSVG) console.log(svg)
}else{
console.log("id :",item.tokenId)
}
}
function parseNFT(item:any){
const data = base64.decode(item.tokenUri.slice(29))
const itemInfo = JSON.parse(data)
const svg = base64.decode(itemInfo.image.slice(26))
return(
{"name":itemInfo.name,
"description":itemInfo.description,
"svg":svg})
}
main().catch((error) => {
console.error(error)
process.exitCode = 1
})
What we do in this script:
- deploy BadgeToken NFT and NFTMarketplace
- mint 3 NFT items to address0
- approve 3 NFT items to the market contract
- list 3 NFT items to NFTMarketplace
- transfer Badge #3 to other
- the listed items should be #1,#2
- address1(account1) buy Badge #1
- address1 purchased item should be #1
- print tokenId, name, svg for inspection
Run:
yarn hardhat run src/playMarket.ts
Result:
marketAddress 0x5FbDB2315678afecb367f032d93F642f64180aa3
nftContractAddress 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
Approve marketAddress 0x5FbDB2315678afecb367f032d93F642f64180aa3
==after purchase & Transfer==
id & name: 3 Badge #3
==after delete==
==my list items==
id & name: 1 Badge #1
id & name: 2 Badge #2
==address1 purchased item svg (only one, tokenId =1)==
id & name: 1 Badge #1
<svg xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='xMinYMin meet' viewBox='0 0 350 350'><style>.base { fill: white; font-family: serif; font-size: 300px; }</style><rect width='100%' height='100%' fill='brown' /><text x='100' y='260' class='base'>1</text></svg>
✨ Done in 4.42s.
Task 4.6: Scripts to prepare for webapp
We need to prepare data for the webapp:
- 1-6 by Account#0, 1- Account1, 2- Account#2
- 7-9 by Account#1, 7,8 - Account#0
- In market: 3,4,5,9 (6 delist by Account#0)
- Account#0:Buy 7,8, List:1-5( 6 delisted)
- Account#1:Buy 1, List:7-9
- Account#2:Buy 2, List:n/a
// src/prepare.ts
import { Signer } from "ethers"
import { ethers } from "hardhat"
import { BadgeToken, NFTMarketplace } from "../typechain"
import { tokenAddress, marketAddress } from "./projectsetting"
const _name='BadgeToken'
const _symbol='BADGE'
async function main() {
console.log("======== deploy to a **new** localhost ======")
/* deploy the NFT contract */
const NFT = await ethers.getContractFactory("BadgeToken")
const nftContract:BadgeToken = await NFT.deploy(_name,_symbol)
await nftContract.deployed()
/* deploy the marketplace */
const Market = await ethers.getContractFactory("NFTMarketplace")
const marketContract:NFTMarketplace = await Market.deploy()
console.log("nftContractAddress:",nftContract.address)
console.log("marketAddress :",marketContract.address)
console.log("======== Prepare for webapp dev ======")
console.log("nftContractAddress:",tokenAddress)
console.log("marketAddress :",marketAddress)
console.log("**should be the same**")
let owner:Signer,account1:Signer,account2:Signer
[owner, account1,account2] = await ethers.getSigners()
const address0 = await owner.getAddress()
const address1 = await account1.getAddress()
const address2 = await account2.getAddress()
const market:NFTMarketplace = await ethers.getContractAt("NFTMarketplace", marketAddress)
const nft:BadgeToken = await ethers.getContractAt("BadgeToken", tokenAddress)
const listingFee = await market.getListingFee()
const auctionPrice = ethers.utils.parseUnits('1', 'ether')
console.log("1. == mint 1-6 to account#0")
for(let i=1;i<=6;i++){
await nft.mintTo(address0)
}
console.log("2. == list 1-6 to market")
for(let i=1;i<=6;i++){
await nft.approve(marketAddress,i)
await market.createMarketItem(tokenAddress, i, auctionPrice, { value: listingFee })
}
console.log("3. == mint 7-9 to account#1")
for(let i=7;i<=9;i++){
await nft.connect(account1).mintTo(address1)
}
console.log("4. == list 1-6 to market")
for(let i=7;i<=9;i++){
await nft.connect(account1).approve(marketAddress,i)
await market.connect(account1).createMarketItem(tokenAddress, i, auctionPrice, { value: listingFee })
}
console.log("5. == account#0 buy 7 & 8")
await market.createMarketSale(tokenAddress, 7, { value: auctionPrice})
await market.createMarketSale(tokenAddress, 8, { value: auctionPrice})
console.log("6. == account#1 buy 1")
await market.connect(account1).createMarketSale(tokenAddress, 1, { value: auctionPrice})
console.log("7. == account#2 buy 2")
await market.connect(account2).createMarketSale(tokenAddress, 2, { value: auctionPrice})
console.log("8. == account#0 delete 6")
await market.deleteMarketItem(6)
}
main().catch((error) => {
console.error(error)
process.exitCode = 1
})
Run a stand-alone local testnet in another terminal:
yarn hardhat node
Run:
yarn hardhat run src/prepare.ts --network localhost
Results:
======== deploy to a **new** localhost ======
nftContractAddress: 0x5FbDB2315678afecb367f032d93F642f64180aa3
marketAddress : 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
======== Prepare for webapp dev ======
nftContractAddress: 0x5FbDB2315678afecb367f032d93F642f64180aa3
marketAddress : 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
**should be the same**
1. == mint 1-6 to account#0
2. == list 1-6 to market
3. == mint 7-9 to account#1
4. == list 1-6 to market
5. == account#0 buy 7 & 8
6. == account#1 buy 1
7. == account#2 buy 2
8. == account#0 delete 6
✨ Done in 5.81s.
Task 5: Webapp for NFTMarketplace
Task 5.1: add component ReadNFTMarket
Currently, we query market contract directly instead of using SWR
in this code snippet.
// components/ReadNFTMarket.tsx
import React from 'react'
import { useEffect,useState } from 'react';
import { useWeb3React } from '@web3-react/core'
import { Web3Provider } from '@ethersproject/providers'
import { Contract } from "@ethersproject/contracts";
import { Grid, GridItem, Box, Text, Button } from "@chakra-ui/react"
import { BigNumber, ethers } from 'ethers';
import useSWR from 'swr'
import { addressNFTContract, addressMarketContract } from '../projectsetting'
import CardERC721 from "./CardERC721"
interface Props {
option: number
}
export default function ReadNFTMarket(props:Props){
const abiJSON = require("abi/NFTMarketplace.json")
const abi = abiJSON.abi
const [items,setItems] = useState<[]>()
const { account, active, library} = useWeb3React<Web3Provider>()
// const { data: items} = useSWR([addressContract, 'fetchActiveItems'], {
// fetcher: fetcher(library, abi),
// })
useEffect( () => {
if(! active)
setItems(undefined)
if(!(active && account && library)) return
// console.log(addressContract,abi,library)
const market:Contract = new Contract(addressMarketContract, abi, library);
console.log(market.provider)
console.log(account)
library.getCode(addressMarketContract).then((result:string)=>{
//check whether it is a contract
if(result === '0x') return
switch(props.option){
case 0:
market.fetchActiveItems({from:account}).then((items:any)=>{
setItems(items)
})
break;
case 1:
market.fetchMyPurchasedItems({from:account}).then((items:any)=>{
setItems(items)
})
break;
case 2:
market.fetchMyCreatedItems({from:account}).then((items:any)=>{
setItems(items)
console.log(items)
})
break;
default:
}
})
//called only when changed to active
},[active,account])
async function buyInNFTMarket(event:React.FormEvent,itemId:BigNumber) {
event.preventDefault()
if(!(active && account && library)) return
//TODO check whether item is available beforehand
const market:Contract = new Contract(addressMarketContract, abi, library.getSigner());
const auctionPrice = ethers.utils.parseUnits('1', 'ether')
market.createMarketSale(
addressNFTContract,
itemId,
{ value: auctionPrice}
).catch('error', console.error)
}
const state = ["On Sale","Sold","Inactive"]
return (
<Grid templateColumns='repeat(3, 1fr)' gap={0} w='100%'>
{items
?
(items.length ==0)
?<Box>no item</Box>
:items.map((item:any)=>{
return(
<GridItem key={item.id} >
<CardERC721 addressContract={item.nftContract} tokenId={item.tokenId} ></CardERC721>
<Text fontSize='sm' px={5} pb={1}> {state[item.state]} </Text>
{((item.seller == account && item.buyer == ethers.constants.AddressZero) || (item.buyer == account))
?<Text fontSize='sm' px={5} pb={1}> owned by you </Text>
:<Text></Text>
}
<Box>{
(item.seller != account && item.state == 0)
? <Button width={220} type="submit" onClick={(e)=>buyInNFTMarket(e,item.id)}>Buy this!</Button>
: <Text></Text>
}
</Box>
</GridItem>)
})
:<Box></Box>}
</Grid>
)
}
Task 5.2: add ReadNFTMarket
to index
We add three ReadNFTMarket
to index.tsx:
- one for all market items
- one for my purchased items
- one for my created items
Task 5.3: Run the DApp
STEP 1: run a new local testnet
In another terminal, run in chain/
yarn hardhat node
STEP 2: prepare data for webapp
Run in chain/
yarn hardhat run src/prepare.ts --network localhost
STEP 3: run webapp
Run in webapp/
yarn dev
STEP 4: browser http://localhost:3000/
and connect MetaMask
Set your MetaMask's mnemonics the Hardhat pre-defined ref link and add the accounts in it:
test test test test test test
test test test test test junk
STEP 5: buy Badge #9 as Account#0
STEP 6: switch to Account#1 in MetaMask, buy Badge #3
Now you have an NFT marketplace. Congratulations.
You can continue to deploy it to public testnet(ropsten), ethereum mainnet, sidechain(BSC/Polygon), Layer2(Arbitrum/Optimism).
Optional Task 6: Deploy to Polygon and query using Alchemy NFT API
Task 6.1 Deploy to Polygon
In this optional task, I will deploy the NFT contract and the NFTMarketplace contract to Polygon mainnet as the gas fee is ok. You can also choose to deploy to Ethereum testnet(Goerli), Polygon testnet(Mumbai) or Layer 2 testnet(such as Arbitrum Goerli).
STEP 1. Edit .env
with Alchemy URL with key, your private key for testing, Polygonscan API key. You may need to add polygon in your hardhat.config.ts
POLYGONSCAN_API_KEY=ABC123ABC123ABC123ABC123ABC123ABC1
POLYGON_URL=https://polygon-mainnet.g.alchemy.com/v2/<YOUR ALCHEMY KEY>
POLYGON_PRIVATE_KEY=0xabc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc1
STEP 2. Deploy the NFT contract and verify on polygonscan.com. Run:
yarn hardhat run scripts/deploy_BadgeToken.ts --network polygon
// BadgeToken deployed to: 0x1FC8b9DC757FD50Bfec8bbE103256F176435faEE
yarn hardhat verify --network polygon 0x1FC8b9DC757FD50Bfec8bbE103256F176435faEE 'BadgeToken' 'BADGE'
// Successfully verified contract BadgeToken on Etherscan.
// https://polygonscan.com/address/0x1FC8b9DC757FD50Bfec8bbE103256F176435faEE#code
STEP 3. Deploy the NFTMarketplace and verify
yarn hardhat run scripts/deploy_Marketplace.ts --network polygon
// NFTMarketplace deployed to: 0x2B7302B1ABCD30Cd475d78688312529027d57bEf
yarn hardhat verify --network polygon 0x2B7302B1ABCD30Cd475d78688312529027d57bEf
// Successfully verified contract NFTMarketplace on Etherscan.
// https://polygonscan.com/address/0x2B7302B1ABCD30Cd475d78688312529027d57bEf#code
Task 6.2 Mint NFT and list on marketplace
STEP 4. Mint one NFT (tokenId=1) to your testing account on https://polygonscan.com/
You can view the NFT "Badge #1" on opensea: https://opensea.io/assets/matic/0x1fc8b9dc757fd50bfec8bbe103256f176435faee/1
STEP 5. List your NFT item "Badge #1" to NFTMarketpalce contract on https://polygonscan.com/
First you need to approve the NFT item "Badge #1" to the NFTMarketpalce.
Then you call CreateMarketItem()
.
STEP 6. Run the webapp. After connecting the wallet, you can see the item in the market.
Note: remember to edit the NFT contract and NFTMarketpalce contract address in webapp/src/projectsetting.ts
.
Task 6.3 Query NFT using Alchemy NFT APIs
Now, we can switch to using Alchemy NFT APIs(docs link) to query NFT data and display it in our webapp.
Let's try it. We will use Alchemy SDK
here for demonstration.
yarn add alchemy-sdk
The code snippet is adapted from Alchemy NFT APIs docs(link). You will need an Alchemy API Key to run it.
// This script demonstrates access to the NFT API via the Alchemy SDK.
import { Network, Alchemy } from "alchemy-sdk";
import base64 from "base-64"
const settings = {
apiKey: "Your Alchemy API Key",
network: Network.MATIC_MAINNET,
};
const alchemy = new Alchemy(settings);
const addressNFTContract = "0x1FC8b9DC757FD50Bfec8bbE103256F176435faEE"
const owner = await alchemy.nft.getOwnersForNft(addressNFTContract, "1")
console.log("Badge #1 owner:", owner )
// Print NFT metadata returned in the response:
const metadata = await alchemy.nft.getNftMetadata(
addressNFTContract,
"1"
)
console.log("tokenURI:", metadata.tokenUri)
const media = metadata.media[0].raw
console.log("media:", media)
const svg = base64.decode(media.slice(26))
console.log(svg)
Results:
Badge #1 owner: { owners: [ '0x08e2af90ff53a3d3952eaa881bf9b3c05e893462' ] }
tokenURI: {
raw: 'data:application/json;base64,eyJuYW...',
gateway: ''
}
media: data:image/svg+xml;base64,PHN2...
<svg xmlns='http://www.w3.org/2000/svg'
preserveAspectRatio='xMinYMin meet'
viewBox='0 0 350 350'>
<style>.base { fill: white; font-family: serif; font-size: 300px; }</style>
<rect width='100%' height='100%' fill='brown' />
<text x='100' y='260' class='base'>1</text>
</svg>
That is it. We have developed a super simplified version of Opensea including contract and webapp. There is a lot of work to be done. Take one for example:
Your first version of NFTMarketpalce works well. Several weeks later, you find that you need to add new functionality to NFTMarketplace.
A smart contract is immutable. Deploying a new version of NFTMarketplace and asking users to list their NFT to the new contract is not a good idea.
Now you need upgradeable smart contract (proxy contract pattern). You can learn how to develop proxy contract in my another tutorial: Tutorial: write upgradeable smart contract (proxy) using OpenZeppelin.
Tutorial List:
1. A Concise Hardhat Tutorial(3 parts)
https://dev.to/yakult/a-concise-hardhat-tutorial-part-1-7eo
2. Understanding Blockchain with Ethers.js
(5 parts)
https://dev.to/yakult/01-understanding-blockchain-with-ethersjs-4-tasks-of-basics-and-transfer-5d17
3. Tutorial : build your first DAPP with Remix and Etherscan (7 Tasks)
https://dev.to/yakult/tutorial-build-your-first-dapp-with-remix-and-etherscan-52kf
4. Tutorial: build DApp with Hardhat, React and Ethers.js (6 Tasks)
https://dev.to/yakult/a-tutorial-build-dapp-with-hardhat-react-and-ethersjs-1gmi
5. Tutorial: build DAPP with Web3-React and SWR(5 Tasks)
https://dev.to/yakult/tutorial-build-dapp-with-web3-react-and-swr-1fb0
6. Tutorial: write upgradeable smart contract (proxy) using OpenZeppelin(7 Tasks)
7. Tutorial: Build an NFT marketplace DApp like Opensea(5 Tasks)
https://dev.to/yakult/tutorial-build-a-nft-marketplace-dapp-like-opensea-3ng9
If you find this tutorial helpful, follow me at Twitter @fjun99
Top comments (23)
Hi, I came across your tutorials by chance
Impressive work 👍
Hi, Thanks for your post.
I have a question.
Y'know, there are so many collections on OpenSea or other marketplaces.
But if I develop marketplace like above, there would be only one NFT collection.
What if I need to create NFT collections on marketplace?
this is just a demo.
to support multi collections, you need to do more.
you can refer to the newly released opensea protocol - seaport.
It's great.
github.com/ProjectOpenSea/seaport
the current opensea protocol wyvern protocol is also good. but I think you can dive into seaport directly.
Thanks.
But Opensea only works for Ethereum and Solana.
I would like to create a nft marketplace on another blockchain network like Cosmos.
I want to know if the nft collection is stored on a blockchain or in a database like MySQL.
Apps like dappRadar tracks trending nft collections, so I thought there was a protocol for nft collections like ERC721.
Will seaport repository help me find a solution?
@yakult can you suggest a guide or tutorial to getting started building a martketplace with seaport
have not read one. And I will work on one.
that would be awesome
Hi all. If you are just starting to get acquainted with NFT, then you probably have a lot of questions. Or perhaps you have heard of such a thing as blue chips, but do not yet understand what it means, so I want to share with you the article blog.alphaguilty.io/what-is-a-blue... , which explains it in detail. This information will be useful to everyone, including those who already have an NFT project, as they told how to get into the league.
Quality UI design is paramount in the development of decentralized applications (DApps), significantly influencing user experience through intuitive navigation, visual appeal, and adaptability to changes, ultimately fostering trust, brand consistency, and user retention in the dynamic landscape of decentralized technologies. So when creating DAPP be sure to read the article: dapp ui
Hello, Thank you for your content.
I have a question.
"There is a ERC20Permit, but there is no permit in ERC721 yet."
Please tell me about this content
I found this blog somewhere soliditydeveloper.com/erc721-permit
I can't use above method?
I have to use only proxy resister method like as opensea?
If I have time in the next two weeks, I can prepare a tutorial on permit both for ERC20 and ERC721.
Im trying to contact you i have a suspect site that use some Daaps to specific wallets. I lost all my NFTs suffering a wallet draining. Can you check if that site have any bad Daaps? Please contact me my email merclown@gmail.com.
Remember, investing in Bitcoin is not an all-or-nothing decision. You can start with an amount that you are comfortable with, and together, we will grow your investment over time. The world of Bitcoin investment offers exciting opportunities, and I genuinely believe that with my guidance and support, you can confidently embark on this journey towards financial empowerment and potential prosperity. If you’re ready to take the first step or have any questions, please feel free to reach out. I am here to help you every step of the way
Email::: jacquiline.buo@gmail.com
facebook.com/jacquelinebu0
It's a nice, impressive blog for technical users and a great guide. For the non-tech guys who want to create their own NFT Marketplace no-code, check out the app we have launched.
Just have to create a smart contract, upload the nft images and launch a marketplace for free.
imintify.com/
Hi. Can you take a few minutes and read this letter for me?!
Tether Gold (XAUt) is a token that provides you ownership of real physical gold.
By putting gold on a Blockchain, we unlock a variety of characteristics that typically only crypto assets possess.
When you connect to our platform, we offer a $500 deposit bonus on BayBit and commission-free top-ups.
Gold Tether AIRDROP
cutt.ly/IwjVg252
Thanks for the great content.
I got one question tho. if nft has bin buy in another market, how can we cancel or delete the item 'nft' from sale in our market?????
Good questions. I will dig how Opensea implement this logic.
Hey Man! Thanks for the great content.
I got one question tho. Is it the same procedure if I want to build a Dapp for fungable tokens f.e. ERC 20 Tokens?
yes, same process, different logic
Thanks!
In which way is the logic different? Is their even a short explenation?
Some comments have been hidden by the post's author - find out more