创建一个像Opensea一样的NFT市场

2022-11-07 10:06:48 浏览数 (1)

译文出自:登链翻译计划[1] 译者:翻译小组[2] 校对:Tiny 熊[3]

使用 Solidity 和 Web3-React 构建一个像 Opensea 一样的 NFT 市场 DApp 是你开启 web3 之旅的一个好步骤。我们来学习编写一个具有完整功能的智能合约实现一个数字藏品的市场。一个集合的 NFT 是这里交易的数字物品。

本教程包含的内容

  • 任务 1:我们建立什么和项目设置
  • 任务 2:NFT 集合智能合约
  • 任务 3:显示 NFT 项目的网页
  • 任务 4:NFT 市场智能合约
  • 任务 5:NFTM 市场的 Web 应用

Nader Dabit 写了两个版本的How to Build a Full Stack NFT Marketplace - V2 (2022)[4]。受他的想法启发,基于他的智能合约代码库,我编写了这个教程。

你可以阅读我以前的教程,并在之后进行练习。如果没有,我建议你在开始之前阅读以下两篇,因为我不会解释那里已经解释的一些技术。

  • 教程:用 Hardhat、React 和 ethers.js 构建 DApp https://dev.to/yakult/a-tutorial-build-dapp-with-hardhat-react-and-ethersjs-1gmi
  • 教程:用 Web3-React 和 SWR 构建 DApp https://dev.to/yakult/tutorial-build-dapp-with-web3-react-and-swr-1fb0

让我们开始构建吧。

任务 1: 创建项目及设置

任务 1.1: 项目包含三部分:

  • 一个 NFT 智能合约和一个简单的网页来显示 NFT。我们将使用链上 SVG 作为 NFT 项目的图像。我们需要一个 NFT 集合图片,以便在市场合约和前端市场页面中使用。
  • 一个 NFT 市场的智能合约用户可以上架 NFT 及购买 NFT。卖家可以将自己的 NFT 从市场上下架。还需要为前端提供查询功能,以查询市场的数据。我们将尽可能地用单元测试来覆盖这个智能合约。
  • 一个使用 React/Web3-React/SWR 的 NFT 前端市场页面。为了简单起见,我们只构建了一个单页面的 web 市场应用。例如,我们不提供卖家在 webapp 中向市场上架 NFT 的功能。

项目的关键部分创建有数据存储、买卖核心功能和查询功能的市场智能合约(NFTMarketplace)。

核心功能:

代码语言:javascript复制
function createMarketItem(address nftContract,uint256 tokenId,uint256 price) payable
  function deleteMarketItem(uint256 itemId) public
  function createMarketSale(address nftContract,uint256 id) public payable

查询功能:

代码语言:javascript复制
function fetchActiveItems() public view returns (MarketItem[] memory)
  function fetchMyPurchasedItems() public view returns (MarketItem[] memory)
  function fetchMyCreatedItems() public view returns (MarketItem[] memory)

卖方可以使用智能合约来:

  • 授权 NFT 给市场合约
  • 上架一个项目并设置价格
  • (等待买家购买 NFT)
  • 收取价格

当买方在市场上购买时,市场合约则促进购买进行:

  • 买方通过支付价格购买
  • 市场合约简化了购买过程:
    • 将价格金额转移给卖方
    • 卖方将 NFT 转给买方
    • 将上架费用转给市场所有者
    • 将市场项目的状态从 Created 改为 Release

本教程的 GitHub 仓库:

  • 智能合约(hardhat 项目):https://github.com/fjun99/nftmarketplace
  • 使用 React 的网络应用:https://github.com/fjun99/web3app-tutrial-using-web3react(nftmarket分支)
  • 虽然我从 Dabit 的 NFT 市场教程中学到了很多东西,但我们要建立的市场有 3 个主要的区别:
    • Dabit 的 NFT 是一个传统的 NFT,它在 IPFS 上存储图片,而我们的 NFT 在链上存储 SVG 图片。这样是为了使我们的教程简单,因为我们不需要处理设置一个服务器来提供 NFT tokenURI(restful json api),也不需要处理服务器或 IPFS 上的图片存储。
    • 在 Dabit 教程的第一个版本中,他将 NFT ERC721 代币智能合约和市场智能合约分开。在第二个版本中,他选择了在一个智能合约中建立一个具有 maketplace 功能的 NFT ERC721。我们选择在这里将它们分开,因为我们想建立一个通用的市场。
    • 在 Dabit 的教程中,当卖家将一个 NFT 项目上架市场上时,他将 NFT 转移到市场合约中,并等待它被出售。作为一个 NFT 用户,我不喜欢这种模式。我想只授权 NFT 到市场合约。在它被售出之前,该物品仍然在我的地址中。(我也不希望使用setApprovalForAll()来授权我地址中的所有 NFT 到市场合约。我们选择以一个一个的方式来授权)。

任务 1.2:项目设置

第 1 步:创建目录

我们将把项目分成两个子目录,chain用于 hardhat 项目,webapp用于 React/Next.js 项目。

代码语言:javascript复制
--nftmarket
  --chain
  --webapp

第 2 步:创建 Hardhat 项目

chain子目录下,安装hardhat开发环境和@openzeppelin/contracts solidity 库。然后我们启动一个空的 hardhat 项目:

代码语言:javascript复制
yarn init -y
yarn add hardhat
yarn add @openzeppelin/contracts
yarn hardhat

或者,你可以从github repo[5]下载 hardhart 的启动项目,在你的nftmarket目录下:

代码语言:javascript复制
git clone git@github.com:fjun99/chain-tutorial-hardhat-starter.git chain
cd chain
yarn install

第 3 步:创建 React/Next.js webapp 项目

你可以下载一个空的 webapp 脚手架

代码语言:javascript复制
git clone https://github.com/fjun99/webapp-tutorial-scaffold.git webapp
cd webapp
yarn install
yarn dev

你也可以下载本教程的 webapp 代码库:

代码语言:javascript复制
git clone git@github.com:fjun99/web3app-tutrial-using-web3react.git webapp
cd webapp
git checkout nftmarket
yarn install

任务 2:NFT 智能合约

任务 2.1:写一个 NFT 智能合约

我们编写一个 NFT ERC721 智能合约,继承 OpenZeppelin 的 ERC721 实现。我们在这里添加三个功能:

  • 自动递增 tokenId,从 1 开始
  • mintTo(address _to)每个人都可以铸币
  • tokenURI()来实现 token URI 和链上 SVG 图片
代码语言:javascript复制
// 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));
    }
}

我们还添加了一个部署 srcipt scripts/deploy_BadgeToken.ts,使用名字:BadgeToken 和符号:BADGE 来部署它。

代码语言:javascript复制
const token = await BadgeToken.deploy('BadgeToken','BADGE')

任务 2.2: 理解 tokenURI()

让我们解释一下 ERC721 tokenURI()函数的实现:

tokenURI()是 ERC721 标准的一个元数据函数,在 OpenZeppelin 文档中 :

tokenURI(uint256 tokenId) → string 返回 tokenId 代币的统一资源标识符(URI)。

通常 tokenURI 会返回一个 URI。我们可以通过连接 baseURI 和 tokenId 来获得每个 token 的 URI 结果。

在我们的tokenURI()中,将 URI 作为一个 base64 编码的对象返回。

首先,构建对象。对象中的 svg 图片也是 base64 编码的:

代码语言:javascript复制
{
"name":"Badge #1",
"description":"Badge NFT with on-chain SVG image."
"image":"data:image/svg xml;base64,[svg base64 encoded]"
}

然后我们返回 base64 编码的对象:

代码语言:javascript复制
data:application/json;base64,(object base64 encoded)

Webapp 可以通过调用tokenURI(tokenId)获得 URI,并解码以获得名称、描述和 SVG 图片。

这里的 SVG 图片由 LOOT 项目改编的。非常简单,就是在图片中显示 tokenId:

代码语言:javascript复制
<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>

任务 2.3: ERC721 合约的单元测试

让我们为这个合约写一个单元测试脚本:

代码语言:javascript复制
// 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 has 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)
  })
})

运行单元测试:

代码语言:javascript复制
yarn hardhat test test/BadgeToken.test.ts

结果:

代码语言:javascript复制
BadgeToken
    ✓ Should has 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)

我们也可以打印单元测试中得到的 tokenURI,以便检查:

代码语言:javascript复制
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>

任务 3:一个显示 NFT 的网页

任务 3.1:使用Web3-ReactChakra UI设置 webapp 项目

我们将使用 web3 连接框架Web3-React来完成我们的工作。网络应用程序栈:

  • React
  • Next.js
  • Chakra UI
  • Web3-React
  • ethers.js
  • SWR

_app.tsx 内容如下:

代码语言:javascript复制
// 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

我们将在之前的教程中使用ConnectMetamask组件。参考教程:用 Web3-React 和 SWR 构建 DApp[6]

任务 3.2:编写组件来显示 NFT

在这个组件中,我们也使用了SWR,就像我们在教程:用 Web3-React 和 SWR 构建 DApp[7]中做的那样。SWR的获取器在utils/fetcher.tsx中。

代码语言:javascript复制
// 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>
)
}

进行一些解释:

  • 当连接到 Metamask 钱包时,这个组件查询 tokenURI(tokenId)以获得 NFT 项目的名称、描述和 svg 图像。

让我们写一个页面来显示 NFT

代码语言:javascript复制
// 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

任务 3.3: 运行 webapp 项目

第 1 步:运行一个独立的本地测试网

在另一个终端,在chain/目录下运行:

代码语言:javascript复制
yarn hardhat node

第 2 步:将 BadgeToken(ERC721)部署到本地测试网中

代码语言:javascript复制
yarn hardhat run scripts/deploy_BadgeToken.ts --network localhost

结果:

代码语言:javascript复制
Deploying BadgeToken ERC721 token...
BadgeToken deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3

第 3 步:在 hardhat 控制台铸造一个 BadgeToken(tokenId = 1)。

运行 hardhat 控制台连接到本地 testenet

代码语言:javascript复制
yarn hardhat console --network localhost

在控制台中运行:

代码语言:javascript复制
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...'

现在我们有了 NFT 项目。我们将在网页中显示它。

第三步:准备你的 MetaMask

确保你的 MetaMask 链接本地测试网(RPC URL http://localhost:8545和链 ID 31337)。

第 4 步:运行 webapp

webapp/中,运行:

代码语言:javascript复制
yarn dev

在 chrome 浏览器中,进入页面。http://localhost:3000/samplenft

连接 MetaMask,NFT 项目将显示在该页面上。(请注意,图片是懒惰的加载。等待加载完成。

我们可以看到,带有 tokenId1的 NFT Badge #1被正确显示。

任务 4:NFT 市场合约

任务 4.1: 合约数据结构

我们从 Nader Dabit 的教程(V1)中改编了Market.sol智能合约来编写市场合约。但是你应该注意到,我们在这个合约中改变了很多。

我们定义了一个struct MarketItem

代码语言:javascript复制
struct MarketItem {
    uint id;
    address nftContract;
    uint256 tokenId;
    address payable seller;
    address payable buyer;
    uint256 price;
    State state;
  }

每个 MarketItem 可以是三种状态之一:

代码语言:javascript复制
enum State { Created, Release, Inactive }

请注意,我们不能依赖Created状态。如果卖方将 NFT 转让给其他人,或者卖方取消了对 NFT 的授权,该状态仍然是Created,它表明其他人可以在市场上购买。实际上,其他人无法购买它。

所有 MarketItem 都存储在一个映射中。

代码语言:javascript复制
mapping(uint256 => MarketItem) private marketItems;

市场合约有一个所有者,也就是合约部署者。当一个 NFT 在市场上出售时,上架费将被交给市场所有者。

在未来,我们可能会增加功能,将所有权转移到其他地址或多签钱包。为了使教程简单,我们跳过这些功能。

市场合约有一个固定的上架费用。

代码语言:javascript复制
uint256 public listingFee = 0.025 ether;
  function getListingFee() public view returns (uint256)

任务 4.2:编写市场合约函数

市场合约你有两类功能:

核心功能:

代码语言:javascript复制
function createMarketItem(address nftContract,uint256 tokenId,uint256 price) payable
  function deleteMarketItem(uint256 itemId) public
  function createMarketSale(address nftContract,uint256 id) public payable

查询功能:

代码语言:javascript复制
function fetchActiveItems() public view returns (MarketItem[] memory)
  function fetchMyPurchasedItems() public view returns (MarketItem[] memory)
  function fetchMyCreatedItems() public view returns (MarketItem[] memory)

完整的合约代码如下:

代码语言:javascript复制
// 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 "] memory items = new MarketItem[");
    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;
    }
  }

}

这个 NFTMarket 合约可以工作,但它并不理想。至少有两项工作要做:

  • 我们应该在查询函数中加入分页功能。如果市场上有成千上万的物品,查询函数就不能很好地工作。
  • 当我们试图验证一个卖家是否已经将 NFT 项目转让给其他人,或者已经取消授权时,我们会调用nft.getApproved成千上万次。这是很糟糕的做法。我们应该尝试找出一个解决方案。

我们还可能发现,让 webapp 直接从智能合约中查询数据并不是一个好的设计。应该有一个数据索引层。The Graph 协议[8]和 subgraph 可以完成这项工作。你可以在dabit 的 NFT 市场教程[9]中找到关于如何使用 subgraph 的解释。

任务 4.3 NFTMarketplace(核心功能)的单元测试

我们将为 NFTMarketplace 添加两个单元测试脚本:

  • 一个用于核心功能
  • 一个用于查询/获取功能

核心功能的单元测试脚本:

代码语言:javascript复制
// 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))
  })
})

运行:

代码语言:javascript复制
yarn hardhat test test/NFTMarketplace.test.ts

结果:

代码语言:javascript复制
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)

任务 4.4: NFTMarketplace 的单元测试(查询功能)

查询功能的单元测试脚本:

代码语言:javascript复制
// 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

  })

})

运行:

代码语言:javascript复制
yarn hardhat test test/NFTMarketplaceFetch.test.ts

结果:

代码语言:javascript复制
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)

任务 4.5: playMarket.ts开发智能合约的辅助脚本

我们写了一个脚本src/playMarket.ts。在开发和调试过程中,我一次又一次地运行这个脚本。它可以帮助我看到市场合约是否能像我想的那样工作:

代码语言:javascript复制
// 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
})

我们在这个脚本中做什么?

  • 部署 BadgeToken NFT 和 NFTMarketplace
  • 铸造 3 个 NFT 项目到地址 0
  • 授权 3 个 NFT 项目到 NFTMarketplace
  • 上架 3 个 NFT 项目到 NFTMarketplace
  • 将#3 NFT 转移到其他地方
  • 上架的项目应该是#1,#2
  • address1(account1)购买 NFT #1
  • 地址 1 购买的项目应该是#1
  • 打印 tokenId, name, svg 以备检查

运行:

代码语言:javascript复制
yarn hardhat run src/playMarket.ts

结果:

代码语言:javascript复制
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.

任务 4.6:为 webapp 准备的脚本

我们需要为 webapp 准备模拟数据:

代码语言:javascript复制
// 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
})

在另一个终端上运行一个独立的本地测试网。

代码语言:javascript复制
yarn hardhat node

运行:

代码语言:javascript复制
yarn hardhat run src/prepare.ts --network localhost

结果:

代码语言:javascript复制
========   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.

任务 5:NFTMarketplace 的 Webapp

任务 5.1:添加组件ReadNFTMarket

目前,我们在这个代码段中直接查询市场合约,而不是使用SWR

代码语言:javascript复制
// 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>

  )
}

任务 5.2:添加ReadNFTMarket到索引中

我们在 index.tsx 中添加三个ReadNFTMarket

  • 一个用于所有市场 NFT
  • 一个是我购买的 NFT
  • 一个是我创建的 NFT

任务 5.3: 运行 DApp

第 1 步:新的本地测试网络

在另一个终端,在chain/中运行

代码语言:javascript复制
yarn hardhat node

第 2 步:为 webapp 准备数据 在chain/中运行:

代码语言:javascript复制
yarn hardhat run src/prepare.ts --network localhost

第 3 步:运行 webapp

webapp/中运行:

代码语言:javascript复制
yarn dev

第 4 步:浏览器http://localhost:3000/并连接 MetaMask

第 5 步:以 0 号账户购买 NFT#9

第 6 步:切换到 MetaMask 的账户#1,购买 NFT #3

现在你有了一个 NFT 市场。恭喜你。


你可以继续把它部署到公共测试网(ropsten)、以太坊主网、侧链(BSC/Polygon)、Layer2(Arbitrum/Optimism)。

如果现在你需要可升级的智能合约(代理合约模式)。你可以参考教程:使用 OpenZeppelin 编写可升级的智能合约[10]


本翻译由 Duet Protocol[11] 赞助支持。

原文:https://dev.to/yakult/tutorial-build-a-nft-marketplace-dapp-like-opensea-3ng9

参考资料

[1]

登链翻译计划: https://github.com/lbc-team/Pioneer

[2]

翻译小组: https://learnblockchain.cn/people/412

[3]

Tiny 熊: https://learnblockchain.cn/people/15

[4]

How to Build a Full Stack NFT Marketplace - V2 (2022): https://dev.to/dabit3/building-scalable-full-stack-apps-on-ethereum-with-polygon-2cfb

[5]

github repo: https://github.com/fjun99/chain-tutorial-hardhat-starter

[6]

教程:用Web3-React和SWR构建DApp: https://dev.to/yakult/tutorial-build-dapp-with-web3-react-and-swr-1fb0

[7]

教程:用Web3-React和SWR构建DApp: https://dev.to/yakult/tutorial-build-dapp-with-web3-react-and-swr-1fb0

[8]

The Graph协议: https://thegraph.com/

[9]

dabit的NFT市场教程: https://dev.to/dabit3/building-scalable-full-stack-apps-on-ethereum-with-polygon-2cfb

[10]

使用OpenZeppelin编写可升级的智能合约: https://learnblockchain.cn/article/4282

[11]

Duet Protocol: https://duet.finance/?utm_souce=learnblockchain

0 人点赞