• 译文出自:登链翻译计划 [1]

  • 译者:翻译小组 [2]

  • 校对:Tiny 熊 [3]

用 Next.js、Tailwind、Solidity[4]、Hardhat[5]、Ethers.js[6]、IPFS 和 Polygon 建立一个 NFT 数字市场

在上一个以太坊教程以太坊全栈开发完全指南 [7] 中,我介绍了如何使用 Hardhat[8] 和 Ethers.js[9] 等现代工具在以太坊上构建一个基本应用。

在过去几个月里,在像 Polygon[10]、Arbitrum[11] 和 Optimism[12] 这样的以太坊扩容解决方案部署应用正在兴起。这些技术使开发人员能够直接像在以太坊上构建相同的应用程序,并具有更低的 Gas 成本和更快的交易速度等额外好处。

但是介绍在这些解决方案上构建应用的文章还很缺乏,我将为使用这些不同的以太坊扩容解决方案的全栈应用程序建立各种示例项目和教程,本文从 Polygon 的这个项目开始。

本项目的最终源代码,请访问这个代码库 [13]

安装依赖

完成本指南,你必须具备以下条件:

  1. 在你的机器上安装 Node.js

  2. 安装浏览器钱包插件 Metamask

技术栈

在本指南中,我们将使用以下方法构建一个全栈应用:

以太坊层 - Polygon[14] 网络应用框架 - Next.js[15] Solidity 开发环境 - Hardhat[16](Hardhat 中文文档 [17]) 文件存储 - IPFS[18] 以太坊网络客户端库 - Ethers.js[19] (ethers.js 中文文档 [20])

另外将在另一篇文章中讨论如何使用 The Graph 协议 [21] 建立一个更强大的 API 层,以绕过原生区块链层提供的数据访问模式的限制。

关于项目

我们要建立的项目将是 Metaverse Marketplace -- 一个 NFT 数字市场。

在 Polygon 网络上构建应用的全栈开发指南 Metaverse Marketplace

数字市场规则如下

当用户在上架作品时,该作品的所有权将从创建者转移到市场。

当用户购买物品时,购买金额将从买方转给卖方,作品将从市场转给买方。

市场所有者将能够设置一个上架费用。这笔费用将从卖家那里收取,并在任何销售完成后转给合约所有者,使市场所有者能够从市场上成交的任何销售中获得经常性收入。

数字市场逻辑将由两个智能合约组成:

NFT 合约 - 该合约允许用户铸造独特的数字资产。

市场合约 - 该合约允许用户将其数字资产在公开市场上上架出售。

我相信这是一个很好的项目,项目使用的工具、技术和想法也会为这个堆栈上的许多其他类型的应用程序奠定了基础, 如:处理合约层面上的支付、佣金和所有权转让等,以及客户端应用程序如何使用这个智能合约来建立一个性能良好、外观漂亮的用户界面。

除了智能合约之外,我还会向你展示如何建立一个 subgraph,使从智能合约中查询数据更加灵活和高效。正如你将看到的,在数据集上创建视图并启用各种高性能的数据访问模式是很难直接从智能合约中做到的,The Graph[22] 使之变得更加容易。

关于 Polygon

来自官方介绍 [23]: Polygon 是一个协议和一个框架,用于构建和连接兼容以太坊的区块链网络。在以太坊上聚集可扩展的解决方案,支持多链的以太坊生态系统

Polygon 比以太坊快 10 倍左右,并且交易费便宜 10 倍以上 [24]。

好的,但这一切是什么意思呢?

这意味着可以使用原来在以太坊上构建应用程序的知识、工具和技术,为用户构建更快、更便宜的应用程序,不仅提供了更好的用户体验,还为原来在直接在以太坊上构建是不可行的许多类型的应用程序 1 打开了大门。

如前所述,还有许多其他以太坊扩容解决方案,如 Arbitrum[25] 和 Optimism[26],也是类似。这些扩容解决方案 [27] 由于技术上的差异,属于不同的类别:如侧链 [28] ,第 2 层 [29],和状态通道 [30]。

Polygon 之前称为 Matic[31],所以你也会看到在提到他们的生态系统的各个部分时,看到 Matic 这个词被交替使用,因为这个名字仍然被用在各个地方,比如他们的代币和网络名称。

要了解更多关于 Polygon 的信息,还可以看看这篇文章 [32],以及 Polygon 的文档这里 [33]。

现在,我们对项目和相关技术有了一个概述,让我们开始建设吧!。

项目设置

我们将创建一个新的 Next.js 应用程序。打开你的终端,创建或切换到一个新的空目录,运行以下命令:

    npx create-next-app digital-marketplace  

接下来,切换到新的目录,并安装依赖库:

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

设置 Tailwind CSS

我们将使用 Tailwind CSS[34] 进行样式设计,Tailwind 是一个非常实用的 CSS 框架,它使创建优美好看的网站变得容易,我们安装 Tailwind 的依赖:

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

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

    npx tailwindcss init -p  

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

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

配置 Hardhat

从项目的根部初始化一个新的 Hardhat 开发环境:

    npx hardhat      ? What do you want to do? Create a sample project      ? Hardhat project root:   

现在你应该看到在根目录中创建了以下文件和文件夹:

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

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

代码在这里 [35]

    /* hardhat.config.js */      require("@nomiclabs/hardhat-waffle")      const fs = require('fs')      const privateKey = fs.readFileSync(".secret").toString().trim() || "01234567890123456789"      module.exports = {        defaultNetwork: "hardhat",        networks: {          hardhat: {            chainId: 1337          },          mumbai: {            url: "https://rpc-mumbai.matic.today",            accounts: [privateKey]          }        },        solidity: {          version: "0.8.4",          settings: {            optimizer: {              enabled: true,              runs: 200            }          }        }      }  

在这个配置中,配置了本地 Hardhat 开发环境以及 mumbai 测试网 [36]。

你可以通过这里 [37] 阅读更多关于 Matic 网络的信息。

接下来,在你项目的根部创建一个名为 .secret 的文件。暂时让这个文件为空。稍后,我们将用一个测试钱包的私钥来填充它,它将保存一些我们将从 Matic 测试网的水龙头获得的 Matic 代币。

请确保永远不要向 Git 提交任何私钥(请在你的 .gitignore 文件中添加 .secret ),为了更加安全,在使用包含 真实 代币的钱包时,可以考虑储存在临时环境变量中。

智能合约

接下来,我们将创建智能合约,先从独特数字资产的 NFT 合约开始。

contracts 目录下创建一个名为 NFT.sol 的新文件,添加以下代码:

代码在这里 [38]

    // contracts/NFT.sol      // SPDX-License-Identifier: MIT OR Apache-2.0      pragma solidity ^0.8.3;      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 NFT is ERC721URIStorage {          using Counters for Counters.Counter;          Counters.Counter private_tokenIds;          address contractAddress;          constructor(address marketplaceAddress) ERC721("Metaverse Tokens", "METT") {              contractAddress = marketplaceAddress;          }          function createToken(string memory tokenURI) public returns (uint) {             _tokenIds.increment();              uint256 newItemId =_tokenIds.current();             _mint(msg.sender, newItemId);             _setTokenURI(newItemId, tokenURI);              setApprovalForAll(contractAddress, true);              return newItemId;          }      }  

这是一个相当直接的 NFT 智能合约,允许用户铸造独特的数字资产并拥有其所有权。

在这份合约中,我们继承了由 OpenZepplin[39] 实现的 ERC721 标准 [40]

接下来,我们创建市场合约 , 这是一个更大的智能合约。我尽力记录每个函数在做什么,在 contracts 目录下创建一个名为 Market.sol 的新文件 :

代码在这里 [41]

    // contracts/Market.sol      // SPDX-License-Identifier: MIT OR Apache-2.0      pragma solidity ^0.8.3;      import "@openzeppelin/contracts/utils/Counters.sol";      import "@openzeppelin/contracts/security/ReentrancyGuard.sol";      import "@openzeppelin/contracts/token/ERC721/ERC721.sol";      import "hardhat/console.sol";      contract NFTMarket is ReentrancyGuard {        using Counters for Counters.Counter;        Counters.Counter private_itemIds;        Counters.Counter private_itemsSold;        address payable owner;        uint256 listingPrice = 0.025 ether;        constructor() {          owner = payable(msg.sender);        }        struct MarketItem {          uint itemId;          address nftContract;          uint256 tokenId;          address payable seller;          address payable owner;          uint256 price;          bool sold;        }        mapping(uint256 => MarketItem) private idToMarketItem;        event MarketItemCreated (          uint indexed itemId,          address indexed nftContract,          uint256 indexed tokenId,          address seller,          address owner,          uint256 price,          bool sold        );        /* Returns the listing price of the contract */        function getListingPrice() public view returns (uint256) {          return listingPrice;        }        /* Places an item for sale on the marketplace */        function createMarketItem(          address nftContract,          uint256 tokenId,          uint256 price        ) public payable nonReentrant {          require(price > 0, "Price must be at least 1 wei");          require(msg.value == listingPrice, "Price must be equal to listing price");         _itemIds.increment();          uint256 itemId =_itemIds.current();          idToMarketItem[itemId] =  MarketItem(            itemId,            nftContract,            tokenId,            payable(msg.sender),            payable(address(0)),            price,            false          );          IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId);          emit MarketItemCreated(            itemId,            nftContract,            tokenId,            msg.sender,            address(0),            price,            false          );        }        /* Creates the sale of a marketplace item */        /* Transfers ownership of the item, as well as funds between parties */        function createMarketSale(          address nftContract,          uint256 itemId          ) public payable nonReentrant {          uint price = idToMarketItem[itemId].price;          uint tokenId = idToMarketItem[itemId].tokenId;          require(msg.value == price, "Please submit the asking price in order to complete the purchase");          idToMarketItem[itemId].seller.transfer(msg.value);          IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId);          idToMarketItem[itemId].owner = payable(msg.sender);          idToMarketItem[itemId].sold = true;         _itemsSold.increment();          payable(owner).transfer(listingPrice);        }        /* Returns all unsold market items */        function fetchMarketItems() public view returns (MarketItem[] memory) {          uint itemCount =_itemIds.current();          uint unsoldItemCount =_itemIds.current() -_itemsSold.current();          uint currentIndex = 0;          MarketItem[] memory items = new MarketItem[](unsoldItemCount "] memory items = new MarketItem[");          for (uint i = 0; i < itemCount; i++) {            if (idToMarketItem[i + 1].owner == address(0)) {              uint currentId = idToMarketItem[i + 1].itemId;              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 =_itemIds.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 "] memory items = new MarketItem[");          for (uint i = 0; i < totalItemCount; i++) {            if (idToMarketItem[i + 1].owner == msg.sender) {              uint currentId = idToMarketItem[i + 1].itemId;              MarketItem storage currentItem = idToMarketItem[currentId];              items[currentIndex] = currentItem;              currentIndex += 1;            }          }          return items;        }        /* Returns only items a user has created */        function fetchItemsCreated() public view returns (MarketItem[] memory) {          uint totalItemCount =_itemIds.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 "] memory items = new MarketItem[");          for (uint i = 0; i < totalItemCount; i++) {            if (idToMarketItem[i + 1].seller == msg.sender) {              uint currentId = idToMarketItem[i + 1].itemId;              MarketItem storage currentItem = idToMarketItem[currentId];              items[currentIndex] = currentItem;              currentIndex += 1;            }          }          return items;        }      }  

现在,智能合约的代码和环境已经完成,可以尝试测试它了。

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

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

代码在这里 [42]

    /* test/sample-test.js */      describe("NFTMarket", function() {        it("Should create and execute market sales", async function() {          /* deploy the marketplace */          const Market = await ethers.getContractFactory("NFTMarket")          const market = await Market.deploy()          await market.deployed()          const marketAddress = market.address          /* deploy the NFT contract */          const NFT = await ethers.getContractFactory("NFT")          const nft = await NFT.deploy(marketAddress)          await nft.deployed()          const nftContractAddress = nft.address          let listingPrice = await market.getListingPrice()          listingPrice = listingPrice.toString()          const auctionPrice = ethers.utils.parseUnits('1', 'ether')          /* create two tokens */          await nft.createToken("https://www.mytokenlocation.com")          await nft.createToken("https://www.mytokenlocation2.com")          /* put both tokens for sale */          await market.createMarketItem(nftContractAddress, 1, auctionPrice, { value: listingPrice })          await market.createMarketItem(nftContractAddress, 2, auctionPrice, { value: listingPrice })          const [_, buyerAddress] = await ethers.getSigners()          /* execute sale of token to another user */          await market.connect(buyerAddress).createMarketSale(nftContractAddress, 1, { value: auctionPrice})          /* query for and return the unsold items */          items = await market.fetchMarketItems()          items = await Promise.all(items.map(async i => {            const tokenUri = await nft.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)        })      })  

在命令行中运行 npx hardhat test 进行测试:

在 Polygon 网络上构建应用的全栈开发指南 运行测试

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

构建前端

现在,智能合约已经工作并准备就绪,可以开始构建用户界面了。

要考虑的第一件事是设置一个布局,启用一些导航,这些导航将引导进入所有的页面。

打开 pages/_app.js ,用以下代码更新它 :

代码在这里 [43]

    /* pages/_app.js */      import '../styles/globals.css'      import Link from 'next/link'      function MyApp({ Component, pageProps }) {        return (          <p>            <nav className="border-b p-6">              <p className="text-4xl font-bold">Metaverse Marketplacep>              <p className="flex mt-4">                <Link href="/">                  <a className="mr-4 text-pink-500">                    Home                  a>                Link>                <Link href="/create-item">                  <a className="mr-6 text-pink-500">                    Sell Digital Asset                  a>                Link>                <Link href="/my-assets">                  <a className="mr-6 text-pink-500">                    My Digital Assets                  a>                Link>                <Link href="/creator-dashboard">                  <a className="mr-6 text-pink-500">                    Creator Dashboard                  a>                Link>              p>            nav>            <Component {...pageProps} />          p>        )      }      export default MyApp  

导航链接有:主页、出售数字资产页面、查看已购买的资产、还有一个创建者仪表板(用来查看你已经创建的资产以及你已经出售的资产)。

查询合约作品

我们要更新的下一个页面是 pages/index.js 。这是应用程序的主要入口,也会作为查询呈现待售数字资产的页面。

代码在这里 [44]

    /* pages/index.js */      import { ethers } from 'ethers'      import { useEffect, useState } from 'react'      import axios from 'axios'      import Web3Modal from "web3modal"      import {        nftaddress, nftmarketaddress      } from '../config'      import NFT from '../artifacts/contracts/NFT.sol/NFT.json'      import Market from '../artifacts/contracts/Market.sol/NFTMarket.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 tokenContract = new ethers.Contract(nftaddress, NFT.abi, provider)          const marketContract = new ethers.Contract(nftmarketaddress, Market.abi, provider)          const data = await marketContract.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 tokenContract.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(nftmarketaddress, Market.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(nftaddress, 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 marketplaceh1>)        return (          <p className="flex justify-center">            <p className="px-4" style={{ maxWidth: '1600px' }}>              <p className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 pt-4">                {                  nfts.map((nft, i) => (                    <p key={i} className="border shadow rounded-xl overflow-hidden">                      <img hide={nft.image} />                      <p className="p-4">                        <p style={{ height: '64px' }} className="text-2xl font-semibold">{nft.name}p>                        <p style={{ height: '70px', overflow: 'hidden' }}>                          <p className="text-gray-400">{nft.description}p>                        p>                      p>                      <p className="p-4 bg-black">                        <p className="text-2xl mb-4 font-bold text-white">{nft.price} ETHp>                        <button className="w-full bg-pink-500 text-white font-bold py-2 px-12 rounded" onClick={() => buyNft(nft)}>Buybutton>                      p>                    p>                  ))                }              p>            p>          p>        )      }  

当页面加载时,我们查询智能合约中任何仍在销售的作品,并将它们与作品的元数据和购买按钮一起呈现在页面上。

创建和上架作品

接下来,我们创建允许用户创建和上架作品的页面。

在这个页面上要做几件事情:

  1. 用户能够上传和保存文件到 IPFS

  2. 用户能够创建一个新的独特的 NFT。

  3. 用户能够设置作品的元数据和价格,并在市场上上架销售。

在用户创建并上架作品后,他们会被重新引导到主页面,以查看所有出售的作品。

代码在这里 [45]

    /* pages/create-item.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 {        nftaddress, nftmarketaddress      } from '../config'      import NFT from '../artifacts/contracts/NFT.sol/NFT.json'      import Market from '../artifacts/contracts/Market.sol/NFTMarket.json'      export default function CreateItem() {        const [fileUrl, setFileUrl] = useState(null)        const [formInput, updateFormInput] = useState({ price: '', name: '', description: '' })        const router = useRouter()        async function onChange(e) {          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 createMarket() {          const { name, description, price } = formInput          if (!name || !description || !price || !fileUrl) return          /* first, upload 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 file is uploaded to IPFS, pass the URL to save it on Polygon */            createSale(url)          } catch (error) {            console.log('Error uploading file: ', error)          }        }        async function createSale(url) {          const web3Modal = new Web3Modal()          const connection = await web3Modal.connect()          const provider = new ethers.providers.Web3Provider(connection)          const signer = provider.getSigner()          /* next, create the item */          let contract = new ethers.Contract(nftaddress, NFT.abi, signer)          let transaction = await contract.createToken(url)          let tx = await transaction.wait()          let event = tx.events[0]          let value = event.args[2]          let tokenId = value.toNumber()          const price = ethers.utils.parseUnits(formInput.price, 'ether')          /* then list the item for sale on the marketplace */          contract = new ethers.Contract(nftmarketaddress, Market.abi, signer)          let listingPrice = await contract.getListingPrice()          listingPrice = listingPrice.toString()          transaction = await contract.createMarketItem(nftaddress, tokenId, price, { value: listingPrice })          await transaction.wait()          router.push('/')        }        return (          <p className="flex justify-center">            <p 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" hide={fileUrl} />                )              }              <button onClick={createMarket} className="font-bold mt-4 bg-pink-500 text-white rounded p-4 shadow-lg">                Create Digital Asset              button>            p>          p>        )      }  

查看用户购买的作品

Market.sol 智能合约中,我们创建了一个名为 fetchMyNFT 的函数,只返回用户拥有的作品。

pages/my-assets.js 中,我们将使用该函数来获取和渲染它们。

这个功能与查询 pages/index.js 不同,因为需要获得用户的地址,并在合约中使用进行查询:

代码在这里 [46]

    /* pages/my-assets.js */      import { ethers } from 'ethers'      import { useEffect, useState } from 'react'      import axios from 'axios'      import Web3Modal from "web3modal"      import {        nftmarketaddress, nftaddress      } from '../config'      import Market from '../artifacts/contracts/Market.sol/NFTMarket.json'      import NFT from '../artifacts/contracts/NFT.sol/NFT.json'      export default function MyAssets() {        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 marketContract = new ethers.Contract(nftmarketaddress, Market.abi, signer)          const tokenContract = new ethers.Contract(nftaddress, NFT.abi, provider)          const data = await marketContract.fetchMyNFTs()          const items = await Promise.all(data.map(async i => {            const tokenUri = await tokenContract.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 assets ownedh1>)        return (          <p className="flex justify-center">            <p className="p-4">              <p className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 pt-4">                {                  nfts.map((nft, i) => (                    <p key={i} className="border shadow rounded-xl overflow-hidden">                      <img hide={nft.image} className="rounded" />                      <p className="p-4 bg-black">                        <p className="text-2xl font-bold text-white">Price - {nft.price} Ethp>                      p>                    p>                  ))                }              p>            p>          p>        )      }  

创建者仪表板

我们将创建的最后一个页面是创建者仪表板,将允许他们查看他们所创建的所有作品以及他们所销售的作品。

这个页面将使用 Market.sol 智能合约中的 fetchItemsCreated 函数,它只返回与调用函数的用户地址相匹配的作品。

在客户端,我们使用 sold 布尔值将作品过滤到另一个单独的数组中,只向用户显示已经售出的作品。

pages 目录下创建一个名为 creator-dashboard.js 的新文件,代码如下:

代码在这里 [47]

    /* pages/creator-dashboard.js */      import { ethers } from 'ethers'      import { useEffect, useState } from 'react'      import axios from 'axios'      import Web3Modal from "web3modal"      import {        nftmarketaddress, nftaddress      } from '../config'      import Market from '../artifacts/contracts/Market.sol/NFTMarket.json'      import NFT from '../artifacts/contracts/NFT.sol/NFT.json'      export default function CreatorDashboard() {        const [nfts, setNfts] = useState([])        const [sold, setSold] = 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 marketContract = new ethers.Contract(nftmarketaddress, Market.abi, signer)          const tokenContract = new ethers.Contract(nftaddress, NFT.abi, provider)          const data = await marketContract.fetchItemsCreated()          const items = await Promise.all(data.map(async i => {            const tokenUri = await tokenContract.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,              sold: i.sold,              image: meta.data.image,            }            return item          }))          /* create a filtered array of items that have been sold */          const soldItems = items.filter(i => i.sold)          setSold(soldItems)          setNfts(items)          setLoadingState('loaded')        }        if (loadingState === 'loaded' && !nfts.length) return (<h1 className="py-10 px-20 text-3xl">No assets createdh1>)        return (          <p>            <p className="p-4">              <h2 className="text-2xl py-2">Items Createdh2>                <p className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 pt-4">                {                  nfts.map((nft, i) => (                    <p key={i} className="border shadow rounded-xl overflow-hidden">                      <img hide={nft.image} className="rounded" />                      <p className="p-4 bg-black">                        <p className="text-2xl font-bold text-white">Price - {nft.price} Ethp>                      p>                    p>                  ))                }              p>            p>              <p className="px-4">              {                Boolean(sold.length) && (                  <p>                    <h2 className="text-2xl py-2">Items soldh2>                    <p className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 pt-4">                      {                        sold.map((nft, i) => (                          <p key={i} className="border shadow rounded-xl overflow-hidden">                            <img hide={nft.image} className="rounded" />                            <p className="p-4 bg-black">                              <p className="text-2xl font-bold text-white">Price - {nft.price} Ethp>                            p>                          p>                        ))                      }                    p>                  p>                )              }              p>          p>        )      }  

运行项目

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

将合约部署到本地网络上

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

为了使这个脚本的目的更加明确,将 scripts/sample-script.js 的名字更新为 scripts/deploy.js

接下来,用以下代码更新 scripts/deploy.js 中的 main 函数。

    async function main() {        const NFTMarket = await hre.ethers.getContractFactory("NFTMarket");        const nftMarket = await NFTMarket.deploy();        await nftMarket.deployed();        console.log("nftMarket deployed to:", nftMarket.address);        const NFT = await hre.ethers.getContractFactory("NFT");        const nft = await NFT.deploy(nftMarket.address);        await nft.deployed();        console.log("nft deployed to:", nft.address);      }  

这个脚本将把两个合约部署到区块链网络上。

我们将首先在本地网络上测试,然后将其部署到 Mumba 测试网 [48]。

打开终端并运行以下命令,可以启动一个本地网络:

    npx hardhat node  

它创建了一个有 19 个账户的本地网络:

在 Polygon 网络上构建应用的全栈开发指南 接下来,保持节点运行,打开一个单独的终端窗口来部署合约,运行以下命令:

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

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

在 Polygon 网络上构建应用的全栈开发指南 使用这些地址,在项目根部创建一个名为 config.js 的新文件,并添加以下代码,将占位符替换为 CLI 打印出的合约地址。

    export const nftmarketaddress = "nft-contract-address"      export const nftaddress = "market-contract-address"  

将账户导入 MetaMask

你可以将节点创建的账户导入你的 Metamask 钱包,在应用中进行使用,要导入这些账户之一,首先将你的 MetaMask 钱包网络切换到 Localhost 8545。

在 Polygon 网络上构建应用的全栈开发指南 Localhost 网络

接下来,在 MetaMask 中点击账户菜单中的 导入账户

在 Polygon 网络上构建应用的全栈开发指南 MetaMask 导入账户

复制并粘贴一个由 CLI 打印出的 私钥(private keys) ,然后点击 导入(import) 。一旦账户被导入,你应该看到账户中的一些 Eth。

在 Polygon 网络上构建应用的全栈开发指南 MetaMask account

我建议导入 2 到 3 个账户,这样你就可以在用户之间测试各种功能了。

运行应用程序

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

    npm run dev  

测试一下,尝试上架一个作品进行销售,然后切换到另一个账户并购买它。

部署到 Polygon

现在我们已经运行了项目,并在本地进行了测试,让我们把它部署到 Polygon。我们将首先部署到 Mumbai[49],即 Matic 测试网络。

我们需要做的第一件事是将我们钱包中的一个 私钥 保存到 .secrets 文件。

为了获得私钥,你可以使用 Hardhat 给你的一个私钥,或者你可以直接从 MetaMask 导出:

在 Polygon 网络上构建应用的全栈开发指南 私钥

对于生产环境中的应用,我建议不要在代码中硬编码你的私钥,而是将其设置为类似环境变量的东西。

配置网络

接下来,我们需要从本地测试网络切换到 Mumbai Testnet[50]。

要做到这一点,我们需要创建和设置网络配置。

首先,打开 MetaMask,点击设置 :

在 Polygon 网络上构建应用的全栈开发指南 MetaMask 设置

接下来,点击 网络 ,然后点击 添加网络 :

在 Polygon 网络上构建应用的全栈开发指南 新网络

在这里,我们将为孟买测试网络添加以下配置,如这里 [51] 所列。

网络名称 : Mumbai 测试网 新的 RPC 网址 : https://rpc-mumbai.matic.today [52] 链 ID: 80001 货币符号 : Matic

点保存,然后你应该能够切换到并使用新的网络 !

最后,你将需要一些 Testnet Matic 代币,以便与应用程序交互。

要获得这些,你可以访问 Matic Faucet[53],输入你想申请代币的钱包地址。

部署到 Matic/Polygon 网络上

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

要做到这一点,要确保与你部署合约的私钥相关的地址已经收到一些 Matic 代币,以支付交易的 Gas 费用。

运行以下命令部署到 Matic :

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

一旦合约部署完毕,你应该可以在你的项目中更新合约地址,并在新的网络上进行测试 ?!

    npm run dev  

部署到主网

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

主要区别在于,其节点不一样,参考这里 [54],以及将网络导入你的 MetaMask 钱包,

在你的项目中,加入类似这样的配置:

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

像上面列出的公共 RPC 可能有流量或速率限制,这取决于使用情况。你可以使用 Infura、MaticVigil、QuickNode、Chainstack 或 Ankr 等服务注册一个专用的免费 RPC URL。

例如,使用 Infura:

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

项目的最终源代码,请访问这个代码库 [55]

接下来

祝贺你!你已经在 Polygon 上部署了一个不复杂的应用程序。

与 Polygon 这样的解决方案合作,最酷的事情是,与直接在以太坊上构建相比,额外工作或学习很少。在这些第二层和侧链中,几乎所有的 API 和工具都是一样的,这使得任何技能都可以在像 Polygon 这样的不同平台上转移。

接下来,我建议使用 The Graph[56] 来移植这个应用程序中实现的查询。图形将开辟更多的数据访问模式,包括分页、过滤和排序等对任何现实世界的应用都是必要的。

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


本翻译由 CellETF[57] 赞助支持。

来源: https://dev.to/dabit3/building-scalable-full-stack-apps-on-ethereum-with-polygon-2cfb

参考资料

[1]

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

[2]

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

[3]

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

[4]

Solidity: https://learnblockchain.cn/docs/solidity/

[5]

Hardhat: https://learnblockchain.cn/docs/hardhat/getting-started/

[6]

Ethers.js: https://learnblockchain.cn/docs/ethers.js/

[7]

以太坊全栈开发完全指南 : https://learnblockchain.cn/article/2383

[8]

Hardhat: https://hardhat.org/

[9]

Ethers.js: https://docs.ethers.io/v5/

[10]

Polygon: https://polygon.technology/

[11]

Arbitrum: https://developer.offchainlabs.com/docs/developer_quickstart

[12]

Optimism: https://optimism.io/

[13]

这个代码库 : https://github.com/dabit3/polygon-ethereum-nextjs-marketplace/

[14]

Polygon: https://polygon.technology/

[15]

Next.js: https://nextjs.org/

[16]

Hardhat: https://hardhat.org/

[17]

Hardhat 中文文档 : https://learnblockchain.cn/docs/hardhat/getting-started/

[18]

IPFS: https://ipfs.io/

[19]

Ethers.js: https://docs.ethers.io/v5/

[20]

ethers.js 中文文档 : https://learnblockchain.cn/docs/ethers.js/

[21]

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

[22]

The Graph: https://thegraph.com/

[23]

官方介绍 : https://polygon.technology/

[24]

Polygon 比以太坊快 10 倍左右,并且交易费便宜 10 倍以上 : https://twitter.com/ChainLinkGod/status/1405688790241316864

[25]

Arbitrum: https://developer.offchainlabs.com/docs/developer_quickstart

[26]

Optimism: https://optimism.io/

[27]

扩容解决方案 : https://ethereum.org/en/developers/docs/scaling/

[28]

侧链 : https://ethereum.org/en/developers/docs/scaling/sidechains/

[29]

第 2 层 : https://ethereum.org/en/developers/docs/scaling/layer-2-rollups/

[30]

状态通道 : https://ethereum.org/en/developers/docs/scaling/state-channels/

[31]

之前称为 Matic: https://cointelegraph.com/news/matic-rebrands-to-polygon-in-pursuit-of-polkadot-on-ethereum-strategy

[32]

这篇文章 : https://medium.com/coinmonks/polygon-matic-could-it-win-the-eth-scaling-race-2c8b4e9baf51

[33]

这里 : https://docs.matic.network/docs/develop/getting-started

[34]

Tailwind CSS: https://tailwindcss.com/

[35]

这里 : https://gist.github.com/dabit3/9c4af9adeb3384e9ae3271181bce0f96

[36]

mumbai 测试网 : https://docs.matic.network/docs/develop/network-details/network/#mumbai-testnet

[37]

这里 : https://docs.matic.network/docs/develop/network-details/network/

[38]

这里 : https://gist.github.com/dabit3/ffdfc03a9279761154eebfea251f0e29

[39]

OpenZepplin: https://docs.openzeppelin.com/contracts/2.x/api/token/erc721

[40]

ERC721 标准 : https://eips.ethereum.org/EIPS/eip-721

[41]

这里 : https://gist.github.com/dabit3/92a572060d62c49707dd0b80378a11ab

[42]

这里 : https://gist.github.com/dabit3/5207fb1a572b699e9eb87aa4845d7a1f

[43]

这里 : https://gist.github.com/dabit3/a076c179ed59743ac238aa6004c80ff3

[44]

这里 : https://gist.github.com/dabit3/3078acab4b3b831fd4a392844d860157

[45]

这里 : https://gist.github.com/dabit3/e3d24ec9fa22f10acecbfa75eb1e4284

[46]

这里 : https://gist.github.com/dabit3/3062d11868124eceb5eb12e2d70609aa

[47]

这里 : https://gist.github.com/dabit3/feea5e27010e298cfeb16e80ba76850f

[48]

Mumba 测试网 : https://docs.matic.network/docs/develop/network-details/network/

[49]

Mumbai: https://docs.matic.network/docs/develop/network-details/network/#mumbai-testnet

[50]

Mumbai Testnet: https://docs.matic.network/docs/develop/network-details/network/#mumbai-testnet

[51]

这里 : https://docs.matic.network/docs/develop/network-details/network/#mumbai-testnet

[52]

https://rpc-mumbai.matic.today:_https://rpc-mumbai.matic.today/ _

[53]

Matic Faucet: https://faucet.matic.network/

[54]

这里 : https://docs.matic.network/docs/develop/network-details/network/

[55]

这个代码库 : https://github.com/dabit3/polygon-ethereum-nextjs-marketplace/

[56]

The Graph: https://thegraph.com/

[57]

CellETF: https://celletf.io/?utm_souce=learnblockchain