如何构建全栈 NFT 市场 - V2(2022 年)

2025-05-24

如何构建全栈 NFT 市场 - V2(2022 年)

使用 Polygon、Next.js、Tailwind、Solidity、Hardhat、Ethers.js 和 IPFS 构建 NFT 市场

要查看本教程的视频课程,请点击此处。要查看本教程的版本 1,请点击此处

在我上一篇端到端以太坊教程《全栈以太坊开发完整指南》中,我介绍了如何使用HardhatEthers.js等现代工具在以太坊上构建一个基本的应用程序

在本指南中,您将学习如何在以太坊上构建、部署和测试一个全栈 NFT 市场。我们还将介绍如何部署到 Polygon。

显而易见的是,PolygonArbitrumOptimism等以太坊扩容解决方案正在迅速发展壮大并被广泛采用。这些技术使开发者能够构建与直接在以太坊上构建相同的应用程序,同时还具有更低的 Gas 成本和更快的交易速度等额外优势。

由于这些解决方案提供的价值主张与现有内容的普遍缺乏相结合,我将使用这些不同的以太坊扩展解决方案为全栈应用程序构建各种示例项目和教程,从 Polygon 上的这个开始。

要查看该项目的最终源代码,请访问此 repo

先决条件

要成功完成本指南,您必须具备以下条件:

  1. 16.14.0您的计算机上已安装Node.js 或更高版本。我建议使用nvmfnm安装 Node 。
  2. Metamask 钱包扩展作为浏览器扩展安装

堆栈

在本指南中,我们将使用以下内容构建一个全栈应用程序:

Web 应用程序框架- Next.js
Solidity 开发环境- Hardhat
文件存储- IPFS
以太坊 Web 客户端库- Ethers.js

虽然它不会成为本指南的一部分(将在单独的文章中介绍),但我们将研究如何使用Graph 协议构建更强大的 API 层,以绕过本机区块链层提供的数据访问模式的限制。

关于项目

我们将要构建的项目是Metaverse Marketplace——一个 NFT 市场。

元宇宙市场

当用户出售 NFT 时,该物品的所有权将从创建者转移到市场合约。

当用户购买 NFT 时,购买价格将从买方转移到卖方,并且物品将从市场转移到买方。

市场所有者可以设定上架费用。该费用将从卖家处收取,并在任何交易完成后转给合同所有者,从而使市场所有者能够从市场中任何交易中获得经常性收入。

市场逻辑仅包含一个智能合约:

NFT 市场合约——合约允许用户铸造 NFT 并将其列在市场上。

我相信这是一个很好的项目,因为我们将要使用的工具、技术和想法为这个堆栈上的许多其他类型的应用程序奠定了基础——处理合同级别的付款、佣金和所有权转移等事务,以及客户端应用程序如何使用这个智能合约来构建高性能且美观的用户界面。

除了智能合约之外,我还将向您展示如何构建子图,以使从智能合约查询数据更加灵活高效。正如您将看到的,直接在智能合约中创建数据集视图并启用各种高性能数据访问模式非常困难。Graph这一切变得简单得多。

关于 Polygon

来自文档

Polygon 是一个用于构建和连接与以太坊兼容的区块链网络的协议和框架。它在以太坊上聚合可扩展的解决方案,支持多链以太坊生态系统。

Polygon 的速度比以太坊快 10 倍,但交易成本却便宜 10 倍以上

好的,很酷,但是这一切意味着什么呢?

对我来说,这意味着我可以使用在以太坊上构建应用程序时所用的知识、工具和技术来构建对用户来说更快、更便宜的应用程序,不仅提供更好的用户体验,而且还为许多无法直接在以太坊上构建的应用程序打开了大门。

如前所述,还有许多其他以太坊扩容解决方案,例如ArbitrumOptimism,它们也处于类似的领域。这些扩容解决方案大多存在技术差异,并分为不同的类别,例如侧链第 2 层状态通道

Polygon最近从 Matic 更名,因此您也会看到Matic这个词在指代其生态系统的各个部分时互换使用,因为该名称仍在各个地方使用,例如其代币和网络名称。

要了解有关 Polygon 的更多信息,请查看此文章以及此处的文档。

现在我们已经对项目和相关技术有了概述,让我们开始构建吧!

项目设置

首先,我们将创建一个新的 Next.js 应用。为此,请打开终端。创建或切换到一个新的空目录,然后运行以下命令:



npx create-next-app nft-marketplace


Enter fullscreen mode Exit fullscreen mode

接下来,进入新目录并使用包管理器(如、或)安装npm依赖yarnpnpm



cd nft-marketplace

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


Enter fullscreen mode Exit fullscreen mode

设置 Tailwind CSS

我们将使用Tailwind CSS进行样式设置,我们将在此步骤中进行设置。

Tailwind 是一个实用优先的 CSS 框架,可以轻松添加样式并创建美观的网站,而无需大量工作。

接下来,安装 Tailwind 依赖项:



npm install -D tailwindcss@latest postcss@latest autoprefixer@latest


Enter fullscreen mode Exit fullscreen mode

接下来,我们将通过运行以下命令创建 Tailwind 与 Next.js(tailwind.config.js和)协同工作所需的配置文件:postcss.config.js



npx tailwindcss init -p


Enter fullscreen mode Exit fullscreen mode

content接下来,在tailwind.config.js中配置模板路径



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


Enter fullscreen mode Exit fullscreen mode

最后,删除styles/globals.css中的代码,并用以下内容更新它:



@tailwind base;
@tailwind components;
@tailwind utilities;


Enter fullscreen mode Exit fullscreen mode

配置安全帽

接下来,从项目根目录初始化一个新的 Hardhat 开发环境:



npx hardhat

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


Enter fullscreen mode Exit fullscreen mode

如果引用README.md时出现错误,请删除README.mdnpx hardhat并再次运行。

现在您应该看到在根目录中为您创建的以下文件和文件夹:

hardhat.config.js - 您的整个 Hardhat 设置(即您的配置、插件和自定义任务)都包含在此文件中。
scripts - 包含名为 sample-script.js 的脚本的文件夹,该脚本将在执行时部署您的智能合约
test - 包含示例测试脚本的文件夹
contract - 包含示例 Solidity 智能合约的文件夹

接下来,使用以下内容更新hardhat.config.js中的配置:

点击此处查看要点



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

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


Enter fullscreen mode Exit fullscreen mode

在这个配置中,我们配置了本地Hardhat开发环境以及孟买测试网(暂时已注释掉)。

您可以在此处阅读有关这两个 Matic 网络的更多信息

智能合约

接下来,我们将创建我们的智能合约!

在这个文件中,我将尽力对代码中发生的一切进行评论。

在contract目录中创建一个名为NFTMarketplace.sol的新文件。在这里添加以下代码:

点击此处查看要点



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

import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

import "hardhat/console.sol";

contract NFTMarketplace is ERC721URIStorage {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;
    Counters.Counter private _itemsSold;

    uint256 listingPrice = 0.025 ether;
    address payable owner;

    mapping(uint256 => MarketItem) private idToMarketItem;

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

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

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

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

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

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

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

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

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

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

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

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

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

    /* Returns all unsold market items */
    function fetchMarketItems() public view returns (MarketItem[] memory) {
      uint itemCount = _tokenIds.current();
      uint unsoldItemCount = _tokenIds.current() - _itemsSold.current();
      uint currentIndex = 0;

      MarketItem[] memory items = new MarketItem[](unsoldItemCount);
      for (uint i = 0; i < itemCount; i++) {
        if (idToMarketItem[i + 1].owner == address(this)) {
          uint currentId = i + 1;
          MarketItem storage currentItem = idToMarketItem[currentId];
          items[currentIndex] = currentItem;
          currentIndex += 1;
        }
      }
      return items;
    }

    /* Returns only items that a user has purchased */
    function fetchMyNFTs() public view returns (MarketItem[] memory) {
      uint totalItemCount = _tokenIds.current();
      uint itemCount = 0;
      uint currentIndex = 0;

      for (uint i = 0; i < totalItemCount; i++) {
        if (idToMarketItem[i + 1].owner == msg.sender) {
          itemCount += 1;
        }
      }

      MarketItem[] memory items = new MarketItem[](itemCount);
      for (uint i = 0; i < totalItemCount; i++) {
        if (idToMarketItem[i + 1].owner == msg.sender) {
          uint currentId = i + 1;
          MarketItem storage currentItem = idToMarketItem[currentId];
          items[currentIndex] = currentItem;
          currentIndex += 1;
        }
      }
      return items;
    }

    /* Returns only items a user has listed */
    function fetchItemsListed() public view returns (MarketItem[] memory) {
      uint totalItemCount = _tokenIds.current();
      uint itemCount = 0;
      uint currentIndex = 0;

      for (uint i = 0; i < totalItemCount; i++) {
        if (idToMarketItem[i + 1].seller == msg.sender) {
          itemCount += 1;
        }
      }

      MarketItem[] memory items = new MarketItem[](itemCount);
      for (uint i = 0; i < totalItemCount; i++) {
        if (idToMarketItem[i + 1].seller == msg.sender) {
          uint currentId = i + 1;
          MarketItem storage currentItem = idToMarketItem[currentId];
          items[currentIndex] = currentItem;
          currentIndex += 1;
        }
      }
      return items;
    }
}


Enter fullscreen mode Exit fullscreen mode

在此合约中,我们继承了OpenZepplin实现的ERC721 标准

现在智能合约代码和环境已经完成,我们可以尝试测试一下。

为此,我们可以创建一个本地测试来运行大部分功能,例如铸造代币、将其出售、将其出售给用户以及查询代币。

要创建测试,请打开test/sample-test.js并使用以下代码更新它:

点击此处查看要点



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

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

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

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

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

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

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

    /* query for and return the unsold items */
    items = await nftMarketplace.fetchMarketItems()
    items = await Promise.all(items.map(async i => {
      const tokenUri = await nftMarketplace.tokenURI(i.tokenId)
      let item = {
        price: i.price.toString(),
        tokenId: i.tokenId.toString(),
        seller: i.seller,
        owner: i.owner,
        tokenUri
      }
      return item
    }))
    console.log('items: ', items)
  })
})


Enter fullscreen mode Exit fullscreen mode

接下来,从命令行运行测试:



npx hardhat test


Enter fullscreen mode Exit fullscreen mode

如果测试成功运行,它应该输出包含两个市场项目的数组。

运行测试

构建前端

现在智能合约已运行并准备就绪,我们可以开始构建 UI。

我们首先要考虑的是设置布局,以便我们可以启用一些在所有页面上都持续存在的导航。

要进行此项设置,请打开pages/_app.js并使用以下代码进行更新:

点击此处查看要点



/* pages/_app.js */
import '../styles/globals.css'
import Link from 'next/link'

function MyApp({ Component, pageProps }) {
  return (
    <div>
      <nav className="border-b p-6">
        <p className="text-4xl font-bold">Metaverse Marketplace</p>
        <div className="flex mt-4">
          <Link href="/">
            <a className="mr-4 text-pink-500">
              Home
            </a>
          </Link>
          <Link href="/create-nft">
            <a className="mr-6 text-pink-500">
              Sell NFT
            </a>
          </Link>
          <Link href="/my-nfts">
            <a className="mr-6 text-pink-500">
              My NFTs
            </a>
          </Link>
          <Link href="/dashboard">
            <a className="mr-6 text-pink-500">
              Dashboard
            </a>
          </Link>
        </div>
      </nav>
      <Component {...pageProps} />
    </div>
  )
}

export default MyApp


Enter fullscreen mode Exit fullscreen mode

导航包含主页链接以及出售 NFT 的页面、查看已购买的 NFT 的页面以及查看已列出的 NFT 的仪表板。

查询合约中的市场商品

我们要更新的下一个页面是pages/index.js。这是应用程序的主要入口点,也是我们查询待售 NFT 并将其渲染到屏幕上的视图。

点击此处查看要点



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

import {
  marketplaceAddress
} from '../config'

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

export default function Home() {
  const [nfts, setNfts] = useState([])
  const [loadingState, setLoadingState] = useState('not-loaded')
  useEffect(() => {
    loadNFTs()
  }, [])
  async function loadNFTs() {
    /* create a generic provider and query for unsold market items */
    const provider = new ethers.providers.JsonRpcProvider()
    const contract = new ethers.Contract(marketplaceAddress, NFTMarketplace.abi, provider)
    const data = await contract.fetchMarketItems()

    /*
    *  map over items returned from smart contract and format 
    *  them as well as fetch their token metadata
    */
    const items = await Promise.all(data.map(async i => {
      const tokenUri = await contract.tokenURI(i.tokenId)
      const meta = await axios.get(tokenUri)
      let price = ethers.utils.formatUnits(i.price.toString(), 'ether')
      let item = {
        price,
        tokenId: i.tokenId.toNumber(),
        seller: i.seller,
        owner: i.owner,
        image: meta.data.image,
        name: meta.data.name,
        description: meta.data.description,
      }
      return item
    }))
    setNfts(items)
    setLoadingState('loaded') 
  }
  async function buyNft(nft) {
    /* needs the user to sign the transaction, so will use Web3Provider and sign it */
    const web3Modal = new Web3Modal()
    const connection = await web3Modal.connect()
    const provider = new ethers.providers.Web3Provider(connection)
    const signer = provider.getSigner()
    const contract = new ethers.Contract(marketplaceAddress, NFTMarketplace.abi, signer)

    /* user will be prompted to pay the asking proces to complete the transaction */
    const price = ethers.utils.parseUnits(nft.price.toString(), 'ether')   
    const transaction = await contract.createMarketSale(nft.tokenId, {
      value: price
    })
    await transaction.wait()
    loadNFTs()
  }
  if (loadingState === 'loaded' && !nfts.length) return (<h1 className="px-20 py-10 text-3xl">No items in marketplace</h1>)
  return (
    <div className="flex justify-center">
      <div className="px-4" style={{ maxWidth: '1600px' }}>
        <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 pt-4">
          {
            nfts.map((nft, i) => (
              <div key={i} className="border shadow rounded-xl overflow-hidden">
                <img src={nft.image} />
                <div className="p-4">
                  <p style={{ height: '64px' }} className="text-2xl font-semibold">{nft.name}</p>
                  <div style={{ height: '70px', overflow: 'hidden' }}>
                    <p className="text-gray-400">{nft.description}</p>
                  </div>
                </div>
                <div className="p-4 bg-black">
                  <p className="text-2xl font-bold text-white">{nft.price} ETH</p>
                  <button className="mt-4 w-full bg-pink-500 text-white font-bold py-2 px-12 rounded" onClick={() => buyNft(nft)}>Buy</button>
                </div>
              </div>
            ))
          }
        </div>
      </div>
    </div>
  )
}


Enter fullscreen mode Exit fullscreen mode

当页面加载时,我们会向智能合约查询仍在出售的 NFT,并将它们与有关物品的元数据和购买按钮一起呈现到屏幕上。

创建和上架 NFT

接下来,让我们创建允许用户创建和列出 NFT 的页面。

此页面上发生了一些事情:

  1. 用户可以将文件上传并保存到 IPFS
  2. 用户可以创建新的 NFT
  3. 用户可以设置商品的元数据和价格,并将其列在市场上出售

用户创建并列出物品后,他们会被重新路由到主页查看所有待售物品。

点击此处查看要点



/* pages/create-nft.js */
import { useState } from 'react'
import { ethers } from 'ethers'
import { create as ipfsHttpClient } from 'ipfs-http-client'
import { useRouter } from 'next/router'
import Web3Modal from 'web3modal'

const client = ipfsHttpClient('https://ipfs.infura.io:5001/api/v0')

import {
  marketplaceAddress
} from '../config'

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

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

  async function onChange(e) {
    /* upload image to IPFS */
    const file = e.target.files[0]
    try {
      const added = await client.add(
        file,
        {
          progress: (prog) => console.log(`received: ${prog}`)
        }
      )
      const url = `https://ipfs.infura.io/ipfs/${added.path}`
      setFileUrl(url)
    } catch (error) {
      console.log('Error uploading file: ', error)
    }  
  }
  async function uploadToIPFS() {
    const { name, description, price } = formInput
    if (!name || !description || !price || !fileUrl) return
    /* first, upload metadata to IPFS */
    const data = JSON.stringify({
      name, description, image: fileUrl
    })
    try {
      const added = await client.add(data)
      const url = `https://ipfs.infura.io/ipfs/${added.path}`
      /* after metadata is uploaded to IPFS, return the URL to use it in the transaction */
      return url
    } catch (error) {
      console.log('Error uploading file: ', error)
    }  
  }

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

    /* create the NFT */
    const price = ethers.utils.parseUnits(formInput.price, 'ether')
    let contract = new ethers.Contract(marketplaceAddress, NFTMarketplace.abi, signer)
    let listingPrice = await contract.getListingPrice()
    listingPrice = listingPrice.toString()
    let transaction = await contract.createToken(url, price, { value: listingPrice })
    await transaction.wait()

    router.push('/')
  }

  return (
    <div className="flex justify-center">
      <div className="w-1/2 flex flex-col pb-12">
        <input 
          placeholder="Asset Name"
          className="mt-8 border rounded p-4"
          onChange={e => updateFormInput({ ...formInput, name: e.target.value })}
        />
        <textarea
          placeholder="Asset Description"
          className="mt-2 border rounded p-4"
          onChange={e => updateFormInput({ ...formInput, description: e.target.value })}
        />
        <input
          placeholder="Asset Price in Eth"
          className="mt-2 border rounded p-4"
          onChange={e => updateFormInput({ ...formInput, price: e.target.value })}
        />
        <input
          type="file"
          name="Asset"
          className="my-4"
          onChange={onChange}
        />
        {
          fileUrl && (
            <img className="rounded mt-4" width="350" src={fileUrl} />
          )
        }
        <button onClick={listNFTForSale} className="font-bold mt-4 bg-pink-500 text-white rounded p-4 shadow-lg">
          Create NFT
        </button>
      </div>
    </div>
  )
}


Enter fullscreen mode Exit fullscreen mode

仅查看用户购买的 NFT

NFTMarketplace.sol智能合约中,我们创建了一个名为 的函数,fetchMyNFTs该函数仅返回用户拥有的 NFT。

pages/my-nfts.js中,我们将使用该函数来获取并呈现它们。

此功能与查询主页/index.js页面不同,因为我们需要向用户询问他们的地址并在合同中使用它,因此用户必须签署交易才能正确获取它们。

点击此处查看要点



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

import {
  marketplaceAddress
} from '../config'

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

export default function MyAssets() {
  const [nfts, setNfts] = useState([])
  const [loadingState, setLoadingState] = useState('not-loaded')
  const router = useRouter()
  useEffect(() => {
    loadNFTs()
  }, [])
  async function loadNFTs() {
    const web3Modal = new Web3Modal({
      network: "mainnet",
      cacheProvider: true,
    })
    const connection = await web3Modal.connect()
    const provider = new ethers.providers.Web3Provider(connection)
    const signer = provider.getSigner()

    const marketplaceContract = new ethers.Contract(marketplaceAddress, NFTMarketplace.abi, signer)
    const data = await marketplaceContract.fetchMyNFTs()

    const items = await Promise.all(data.map(async i => {
      const tokenURI = await marketplaceContract.tokenURI(i.tokenId)
      const meta = await axios.get(tokenURI)
      let price = ethers.utils.formatUnits(i.price.toString(), 'ether')
      let item = {
        price,
        tokenId: i.tokenId.toNumber(),
        seller: i.seller,
        owner: i.owner,
        image: meta.data.image,
        tokenURI
      }
      return item
    }))
    setNfts(items)
    setLoadingState('loaded') 
  }
  function listNFT(nft) {
    router.push(`/resell-nft?id=${nft.tokenId}&tokenURI=${nft.tokenURI}`)
  }
  if (loadingState === 'loaded' && !nfts.length) return (<h1 className="py-10 px-20 text-3xl">No NFTs owned</h1>)
  return (
    <div className="flex justify-center">
      <div className="p-4">
        <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 pt-4">
          {
            nfts.map((nft, i) => (
              <div key={i} className="border shadow rounded-xl overflow-hidden">
                <img src={nft.image} className="rounded" />
                <div className="p-4 bg-black">
                  <p className="text-2xl font-bold text-white">Price - {nft.price} Eth</p>
                  <button className="mt-4 w-full bg-pink-500 text-white font-bold py-2 px-12 rounded" onClick={() => listNFT(nft)}>List</button>
                </div>
              </div>
            ))
          }
        </div>
      </div>
    </div>
  )
}


Enter fullscreen mode Exit fullscreen mode

仪表板

我们将要创建的下一页是仪表板,它允许用户查看他们列出的所有项目。

此页面将使用NFTMarketplace.solfetchItemsListed智能合约中的函数,该函数仅返回与调用函数的用户地址匹配的项目。

pages目录中创建一个名为dashboard.js的新文件,内容如下:

点击此处查看要点



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

import {
  marketplaceAddress
} from '../config'

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

export default function CreatorDashboard() {
  const [nfts, setNfts] = useState([])
  const [loadingState, setLoadingState] = useState('not-loaded')
  useEffect(() => {
    loadNFTs()
  }, [])
  async function loadNFTs() {
    const web3Modal = new Web3Modal({
      network: 'mainnet',
      cacheProvider: true,
    })
    const connection = await web3Modal.connect()
    const provider = new ethers.providers.Web3Provider(connection)
    const signer = provider.getSigner()

    const contract = new ethers.Contract(marketplaceAddress, NFTMarketplace.abi, signer)
    const data = await contract.fetchItemsListed()

    const items = await Promise.all(data.map(async i => {
      const tokenUri = await contract.tokenURI(i.tokenId)
      const meta = await axios.get(tokenUri)
      let price = ethers.utils.formatUnits(i.price.toString(), 'ether')
      let item = {
        price,
        tokenId: i.tokenId.toNumber(),
        seller: i.seller,
        owner: i.owner,
        image: meta.data.image,
      }
      return item
    }))

    setNfts(items)
    setLoadingState('loaded') 
  }
  if (loadingState === 'loaded' && !nfts.length) return (<h1 className="py-10 px-20 text-3xl">No NFTs listed</h1>)
  return (
    <div>
      <div className="p-4">
        <h2 className="text-2xl py-2">Items Listed</h2>
          <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 pt-4">
          {
            nfts.map((nft, i) => (
              <div key={i} className="border shadow rounded-xl overflow-hidden">
                <img src={nft.image} className="rounded" />
                <div className="p-4 bg-black">
                  <p className="text-2xl font-bold text-white">Price - {nft.price} Eth</p>
                </div>
              </div>
            ))
          }
        </div>
      </div>
    </div>
  )
}


Enter fullscreen mode Exit fullscreen mode

转售代币

我们将创建的最终页面将允许用户转售从其他人那里购买的 NFT。

此页面将使用resellToken来自NFTMarketplace.sol智能合约的功能。

点击此处查看要点



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

import {
  marketplaceAddress
} from '../config'

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

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

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

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

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

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

    listingPrice = listingPrice.toString()
    let transaction = await contract.resellToken(id, priceFormatted, { value: listingPrice })
    await transaction.wait()

    router.push('/')
  }

  return (
    <div className="flex justify-center">
      <div className="w-1/2 flex flex-col pb-12">
        <input
          placeholder="Asset Price in Eth"
          className="mt-2 border rounded p-4"
          onChange={e => updateFormInput({ ...formInput, price: e.target.value })}
        />
        {
          image && (
            <img className="rounded mt-4" width="350" src={image} />
          )
        }
        <button onClick={listNFTForSale} className="font-bold mt-4 bg-pink-500 text-white rounded p-4 shadow-lg">
          List NFT
        </button>
      </div>
    </div>
  )
}


Enter fullscreen mode Exit fullscreen mode

运行项目

要运行该项目,我们需要一个部署脚本来将智能合约部署到区块链网络。

将合约部署到本地网络

当我们创建项目时,Hardhat 在scripts/sample-script.js创建了一个示例部署脚本。

为了使该脚本的用途更加清晰,请将scripts/sample-script.js的名称更新scripts/deploy.js

接下来,使用以下代码更新scripts/deploy.js :



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

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

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

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


Enter fullscreen mode Exit fullscreen mode

该脚本将把合约部署到区块链网络,并创建一个名为config.js的文件,该文件将保存部署后的智能合约的地址。

我们将首先在本地网络上进行测试,然后将其部署到孟买测试网

要启动本地网络,请打开终端并运行以下命令:



npx hardhat node


Enter fullscreen mode Exit fullscreen mode

这应该创建一个具有 20 个帐户的本地网络。

安全帽节点

接下来,保持节点运行,并打开一个单独的终端窗口来部署合约。

在单独的窗口中,运行以下命令:



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


Enter fullscreen mode Exit fullscreen mode

部署完成后,CLI 应该打印出已部署的合约的地址:

合约地址

您还应该看到config.js文件填充了此智能合约地址。

将账户导入 MetaMask

您可以将节点创建的帐户导入到您的 Metamask 钱包中,以便在应用程序中试用。

每个账户都存有 10,000 ETH。

要导入其中一个帐户,首先将您的 MetaMask 钱包网络切换到 Localhost 8545。

本地主机网络

接下来,在 MetaMask 中,从帐户菜单中单击“导入帐户” :

MetaMask 导入账户

复制并粘贴CLI 注销的私钥之一,然后单击“导入”。导入账户后,您应该会在账户中看到一些 Eth:

MetaMask 账户

我建议使用 2 或 3 个帐户来执行此操作,以便您能够测试用户之间的各种功能。

运行应用程序

现在我们可以测试该应用程序了!

要启动应用程序,请在 CLI 中运行以下命令:



npm run dev


Enter fullscreen mode Exit fullscreen mode

为了测试一切,请尝试列出要出售的物品,然后切换到另一个帐户并购买它。

部署到多边形

现在我们已经启动并运行了项目,并在本地进行了测试,接下来让我们部署到 Polygon。我们将首先部署到Polygon 测试网络Mumbai 。

我们需要做的第一件事是将钱包中的一个私钥保存为环境变量。

要获取私钥,您可以使用 Hardhat 提供给您的私钥之一,也可以直接从 MetaMask 导出它们。

私钥

如果您使用的是 Mac,则可以像这样从命令行设置环境变量(确保从同一个终端和会话运行部署脚本):



export privateKey="your-private-key"


Enter fullscreen mode Exit fullscreen mode

在任何情况下,私钥都不应公开共享。建议切勿将私钥硬编码到文件中。如果您选择这样做,请务必使用测试钱包,并且在任何情况下都不要将包含私钥的文件推送到源代码管理平台或公开。

配置网络

接下来,我们需要从本地测试网络切换到孟买测试网

为此,我们需要创建并设置网络配置。

首先,打开 MetaMask 并点击“设置”

MetaMask 设置

接下来,单击“网络”,然后单击“添加网络”

新网络

在这里,我们将为孟买测试网络添加以下配置,如下所示

网络名称:孟买测试网
新 RPC URL:https://rpc-mumbai.maticvigil.com
链 ID:80001
货币符号:Matic

保存此内容,然后您就可以切换到并使用新网络!

最后,您将需要一些测试网 Matic 代币才能与应用程序进行交互。

要获得这些,您可以访问Matic Faucet,输入您想要请求代币的钱包的地址。

部署到 Matic / Polygon 网络

现在您有了一些 Matic 代币,您可以部署到 Polygon 网络!

为此,请确保与您部署合约的私钥关联的地址已收到一些 Matic 代币,以便支付交易的 gas 费用。

另外,请务必取消注释hardhat.config.jsmumbai中的配置



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


Enter fullscreen mode Exit fullscreen mode

要部署到 Matic,请运行以下命令:



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


Enter fullscreen mode Exit fullscreen mode

如果运行部署错误,公共 RPC 可能会出现拥塞。在生产环境中,建议使用 RPC 提供程序,例如InfuraAlchemyQuicknodeFigment DataHub

部署合约后,更新pages/index.jsloadNFTs中的函数调用以包含新的 RPC 端点:



/* pages/index.js */

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

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


Enter fullscreen mode Exit fullscreen mode

您现在应该能够更新项目中的合约地址并在新网络上进行测试🎉!



npm run dev


Enter fullscreen mode Exit fullscreen mode

如果您遇到错误,由于我最近遇到的一个错误,安全帽打印到控制台的合约地址可能是错误的。您可以通过访问https://mumbai.polygonscan.com/并粘贴部署合约的地址来获取正确的合约地址,以查看最新的交易,并从交易数据中获取合约地址。

部署到主网

要部署到主要的 Matic / Polygon 网络,您可以使用我们为孟买测试网络设置的相同步骤。

主要区别在于您需要使用 Matic 的端点以及将网络导入到您的 MetaMask 钱包中,如此处所列

为实现此目的,项目中的示例更新可能如下所示:



/* hardhat.config.js */

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


Enter fullscreen mode Exit fullscreen mode

像上面列出的公共 RPC 可能会根据使用情况限制流量或速率。您可以使用 Infura、MaticVigil、QuickNode、Alchemy、Chainstack 或 Ankr 等服务注册一个专用的免费 RPC URL。

例如,使用类似 Infura 的东西:



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


Enter fullscreen mode Exit fullscreen mode

要查看该项目的最终源代码,请访问此 repo

后续步骤Next steps

恭喜!您已将一款出色的应用部署到 Polygon。

使用 Polygon 这样的解决方案最酷的地方在于,与直接在以太坊上构建相比,我几乎不需要做任何额外的工作或学习。这些 Layer 2 和侧链中的 API 和工具几乎都保持不变,这使得任何技能都可以在 Polygon 等各种平台上迁移。

对于接下来的步骤,我建议使用The Graph移植此应用中实现的查询。The Graph 将开启更多数据访问模式,包括分页、过滤和排序等任何实际应用所必需的功能。

我还将在未来几周发布一个教程,展示如何将 Polygon 与 The Graph 结合使用。

文章来源:https://dev.to/edge-and-node/building-scalable-full-stack-apps-on-ethereum-with-polygon-2cfb
PREV
使用 React、Anchor、Rust 和 Phantom 进行全栈 Solana 开发的完整指南
NEXT
在以太坊上构建 GraphQL API 去中心化网络基础设施