Web3 Tutorial: Build an NFT marketplace DApp like OpenSea
Tags: blockchain, dapp, nft, solidity
Published: 2022-03-05 01:49:00

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](#task1)
- Task 2: [NFT collection smart contract](#task2)
- Task 3: [A web page to display NFT item](#task3)
- Task 4: [NFT marketplace smart contract](#task4)
- Task 5: [Webapp for NFTMarketplace](#task5)
- Task 6: [Deploy to Polygon and query using Alchemy NFT APIs](#task6)

---

Nader Dabit wrote two versions of [How to Build a Full Stack NFT Marketplace - V2 (2022)](https://dev.to/dabit3/building-scalable-full-stack-apps-on-ethereum-with-polygon-2cfb). 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hbv1t4ryp10inardglqh.png)
 

## Task 1: What we build and project setup <a name="task1"></a>

### 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:

``` js
function createMarketItem(address nftContract,uint256 tokenId,uint256 price) payable 
function deleteMarketItem(uint256 itemId) public
function createMarketSale(address nftContract,uint256 id) public payable
```

Query functions:
``` js
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` to `Release`

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](https://github.com/fjun99/chain-tutorial-hardhat-starter). 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m9mjevootlegapc7p20a.png)
 

## Task 2: NFT collection smart contract<a name="task2"></a>

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

``` js
// 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`:

``` js
  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.

``` json
{
"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.

``` html
<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:

``` ts
// 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3pbg2k1mzrmd6rrty4nd.png)
 

## Task 3: A web page to display NFT item<a name="task3"></a>

### 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:
``` js
// 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 SWR[Tutorial: build DApp with Web3-React and SWR](https://dev.to/yakult/tutorial-build-dapp-with-web3-react-and-swr-1fb0). 

### 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`.

``` js
// 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.

``` js
// 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:
``` js
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.

![nft item](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mni1krtarity4syi9ya0.png)
 

We can see that the NFT "Badge #1" with tokenId `1` is displayed correctly.

---

![task 4](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9yid7iflw78ge604vm8n.png)
 

## Task 4: NFT marketplace smart contract<a name="task4"></a>

### 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`:

``` js
  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:
``` js
  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`:

``` js
  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:

``` js
  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:

``` js
  function createMarketItem(address nftContract,uint256 tokenId,uint256 price) payable 
  function deleteMarketItem(uint256 itemId) public
  function createMarketSale(address nftContract,uint256 id) public payable
```

Query functions:
``` js
  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:

``` js
// 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](https://thegraph.com/) and subgraph can do this job. You can find an explanation on how to use subgraph in [Dabit's NFT market tutorial](https://dev.to/dabit3/building-scalable-full-stack-apps-on-ethereum-with-polygon-2cfb).

---

### 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 calling `transferFrom()`. I would like to choose not to use `setApprovalForAll(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 original `msg.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.

![delegatecall](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lzcfujj9vne99usj7042.png)
 

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.

  - 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 test `await nft.approve(ethers.constants.AddressZero,1)`

Opensea suggests to use `isApprovedForAll` in its tutorial ([sample code](https://github.com/ProjectOpenSea/opensea-creatures/blob/master/contracts/ERC721Tradable.sol)):

``` js
    /**
     * 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](https://docs.opensea.io/docs/polygon-basic-integration#overriding-isapprovedforall-to-reduce-trading-friction) 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:

``` js
// 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:

``` js
// 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. 

``` js
// 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

``` js
// 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2438hbatg83em0wj9wro.png)
 

## Task 5: Webapp for NFTMarketplace<a name="task5"></a>

### Task 5.1: add component `ReadNFTMarket`

Currently, we query market contract directly instead of using `SWR` in this code snippet. 

``` js
// 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

![dapp](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sf7g15s7dv2w2d9fg03a.png)
 

### 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](https://hardhat.org/hardhat-network/docs/reference#accounts) 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).

---

![Task 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8wcepjjin74cyva3ld0t.png)


## Optional Task 6: Deploy to Polygon and query using Alchemy NFT API <a name="task6"></a>

### 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](polygonscan.com). Run:

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

```bash
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`.


![deploy and mint](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6ml1wbztdwkjl7lz71t5.png)

---

### Task 6.3 Query NFT using Alchemy NFT APIs <a name="alchemynft"></a>

Now, we can switch to using Alchemy NFT APIs([docs link](https://docs.alchemy.com/reference/nft-api-quickstart)) to query NFT data and display it in our webapp.

Let's try it. We will use `Alchemy SDK` here for demonstration.

```bash
yarn add alchemy-sdk
```

The code snippet is adapted from Alchemy NFT APIs docs([link](https://docs.alchemy.com/reference/nft-api-quickstart#demo-script)). You will need an Alchemy API Key to run it.

```js
// 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](https://dev.to/yakult/tutorial-write-upgradeable-smart-contract-proxy-contract-with-openzeppelin-1916).

---


### 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)
https://dev.to/yakult/tutorial-write-upgradeable-smart-contract-proxy-contract-with-openzeppelin-1916

#### 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](https://twitter.com/fjun99)