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

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

> * 原文:https://dev.to/dabit3/building-scalable-full-stack-apps-on-ethereum-with-polygon-2cfb > * 译文出自:[登链翻译计划](https://github.com/lbc-team/Pioneer) > * 译者:[翻译小组](https://learnblockchain.cn/people/412) > * 校对:[Tiny 熊](https://learnblockchain.cn/people/15) > * 本文永久链接:[learnblockchain.cn/article…](https://learnblockchain.cn/article/2799) 用Next.js、Tailwind、[Solidity](https://learnblockchain.cn/docs/solidity/)、[Hardhat](https://learnblockchain.cn/docs/hardhat/getting-started/)、[Ethers.js](https://learnblockchain.cn/docs/ethers.js/)、IPFS和Polygon建立一个NFT数字市场 在上一个以太坊教程[以太坊全栈开发完全指南](https://learnblockchain.cn/article/2383)中,我介绍了如何使用[Hardhat](https://hardhat.org/)和[Ethers.js](https://docs.ethers.io/v5/)等现代工具在以太坊上构建一个基本应用。 在过去几个月里,在像[Polygon](https://polygon.technology/)、[Arbitrum](https://developer.offchainlabs.com/docs/developer_quickstart)和[Optimism](https://optimism.io/)这样的以太坊扩容解决方案部署应用正在兴起。这些技术使开发人员能够直接像在以太坊上构建相同的应用程序,并具有更低的Gas成本和更快的交易速度等额外好处。 但是介绍在这些解决方案上构建应用的文章还很缺乏,我将为使用这些不同的以太坊扩容解决方案的全栈应用程序建立各种示例项目和教程,本文从Polygon的这个项目开始。 > 本项目的最终源代码,请访问[这个代码库](https://github.com/dabit3/polygon-ethereum-nextjs-marketplace/) ### 安装依赖 完成本指南,你必须具备以下条件: 1. 在你的机器上安装Node.js 2. 安装浏览器钱包插件Metamask ### 技术栈 在本指南中,我们将使用以下方法构建一个全栈应用: **以太坊层** - [Polygon](https://polygon.technology/) **网络应用框架** - [Next.js](https://nextjs.org/) **Solidity开发环境** - [Hardhat](https://hardhat.org/)([Hardhat中文文档](https://learnblockchain.cn/docs/hardhat/getting-started/)) **文件存储** - [IPFS](https://ipfs.io/) **以太坊网络客户端库** - [Ethers.js](https://docs.ethers.io/v5/) ([ethers.js 中文文档](https://learnblockchain.cn/docs/ethers.js/)) 另外将在另一篇文章中讨论如何使用[The Graph协议](https://thegraph.com/)建立一个更强大的API层,以绕过原生区块链层提供的数据访问模式的限制。 ### 关于项目 我们要建立的项目将是**Metaverse Marketplace **-- 一个NFT数字市场。 ![Metaverse Marketplace](https://img.learnblockchain.cn/pics/20210729085924.png) **数字市场规则如下**: 当用户在上架作品时,该作品的所有权将从创建者转移到市场。 当用户购买物品时,购买金额将从买方转给卖方,作品将从市场转给买方。 市场所有者将能够设置一个上架费用。这笔费用将从卖家那里收取,并在任何销售完成后转给合约所有者,使市场所有者能够从市场上成交的任何销售中获得经常性收入。 数字市场逻辑将由两个智能合约组成: **NFT合约** - 该合约允许用户铸造独特的数字资产。 **市场合约** - 该合约允许用户将其数字资产在公开市场上上架出售。 我相信这是一个很好的项目,项目使用的工具、技术和想法也会为这个堆栈上的许多其他类型的应用程序奠定了基础, 如:处理合约层面上的支付、佣金和所有权转让等,以及客户端应用程序如何使用这个智能合约来建立一个性能良好、外观漂亮的用户界面。 除了智能合约之外,我还会向你展示如何建立一个subgraph,使从智能合约中查询数据更加灵活和高效。正如你将看到的,在数据集上创建视图并启用各种高性能的数据访问模式是很难直接从智能合约中做到的,[The Graph](https://thegraph.com/)使之变得更加容易。 ### 关于Polygon 来自[官方介绍](https://polygon.technology/): **Polygon是一个协议和一个框架,用于构建和连接兼容以太坊的区块链网络。在以太坊上聚集可扩展的解决方案,支持多链的以太坊生态系统**。 [Polygon比以太坊快10倍左右,并且交易费便宜10倍以上](https://twitter.com/ChainLinkGod/status/1405688790241316864)。 好的,但这一切是什么意思呢? 这意味着可以使用原来在以太坊上构建应用程序的知识、工具和技术,为用户构建更快、更便宜的应用程序,不仅提供了更好的用户体验,还为原来在直接在以太坊上构建是不可行的许多类型的应用程序1打开了大门。 如前所述,还有许多其他以太坊扩容解决方案,如[Arbitrum](https://developer.offchainlabs.com/docs/developer_quickstart)和[Optimism](https://optimism.io/),也是类似。这些[扩容解决方案](https://ethereum.org/en/developers/docs/scaling/)由于技术上的差异,属于不同的类别:如[侧链](https://ethereum.org/en/developers/docs/scaling/sidechains/) ,[第2层](https://ethereum.org/en/developers/docs/scaling/layer-2-rollups/),和[状态通道](https://ethereum.org/en/developers/docs/scaling/state-channels/)。 Polygon[之前称为Matic](https://cointelegraph.com/news/matic-rebrands-to-polygon-in-pursuit-of-polkadot-on-ethereum-strategy),所以你也会看到在提到他们的生态系统的各个部分时,看到**Matic**这个词被交替使用,因为这个名字仍然被用在各个地方,比如他们的代币和网络名称。 要了解更多关于Polygon的信息,还可以看看[这篇文章](https://medium.com/coinmonks/polygon-matic-could-it-win-the-eth-scaling-race-2c8b4e9baf51),以及Polygon的文档[这里](https://docs.matic.network/docs/develop/getting-started)。 现在,我们对项目和相关技术有了一个概述,让我们开始建设吧!。 ## 项目设置 我们将创建一个新的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](https://tailwindcss.com/)进行样式设计,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: <Choose default path> ``` 现在你应该看到在根目录中创建了以下文件和文件夹: **hardhat.config.js** - Hardhat设置(即你的配置、插件和自定义任务)都包含在这个文件中。 **scripts** - 一个包含名为sample-script.js的脚本的文件夹,在执行时将部署智能合约。 **test** - 一个包含测试脚本示例的文件夹 **contracts** - 一个包含Solidity智能合约示例的文件夹 接下来,用以下内容更新**hardhat.config.js**的配置: > 代码在[这里](https://gist.github.com/dabit3/9c4af9adeb3384e9ae3271181bce0f96) ```js /* 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测试网**](https://docs.matic.network/docs/develop/network-details/network/#mumbai-testnet)。 > 你可以通过[这里](https://docs.matic.network/docs/develop/network-details/network/)阅读更多关于Matic网络的信息。 接下来,在你项目的根部创建一个名为**.secret**的文件。暂时让这个文件为空。稍后,我们将用一个测试钱包的私钥来填充它,它将保存一些我们将从Matic测试网的水龙头获得的Matic代币。 > 请确保永远不要向Git提交任何私钥(请在你的**.gitignore**文件中添加**.secret**),为了更加安全,在使用包含**真实**代币的钱包时,可以考虑储存在临时环境变量中。 ### 智能合约 接下来,我们将创建智能合约,先从独特数字资产的NFT合约开始。 在**contracts**目录下创建一个名为**NFT.sol**的新文件,添加以下代码: > 代码在[这里](https://gist.github.com/dabit3/ffdfc03a9279761154eebfea251f0e29) ```javascript // 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](https://docs.openzeppelin.com/contracts/2.x/api/token/erc721)实现的[ERC721标准](https://eips.ethereum.org/EIPS/eip-721) 接下来,我们创建市场合约,这是一个更大的智能合约。我尽力记录每个函数在做什么,在**contracts**目录下创建一个名为**Market.sol**的新文件: > 代码在[这里](https://gist.github.com/dabit3/92a572060d62c49707dd0b80378a11ab) ```javascript // 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); 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); 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); 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**,用以下代码更新它: > 代码在[这里](https://gist.github.com/dabit3/5207fb1a572b699e9eb87aa4845d7a1f) ```javascript /* 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`进行测试: ![运行测试](https://img.learnblockchain.cn/pics/20210729090911.png) > 如果测试成功运行,它应该记录出一个包含单一市场项目的数组。 ## 构建前端 现在,智能合约已经工作并准备就绪,可以开始构建用户界面了。 要考虑的第一件事是设置一个布局,启用一些导航,这些导航将引导进入所有的页面。 打开**pages/_app.js**,用以下代码更新它: > 代码在[这里](https://gist.github.com/dabit3/a076c179ed59743ac238aa6004c80ff3) ```javascript /* 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-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> </div> </nav> <Component {...pageProps} /> </div> ) } export default MyApp ``` 导航链接有:主页、出售数字资产页面、查看已购买的资产、还有一个创建者仪表板(用来查看你已经创建的资产以及你已经出售的资产)。 #### 查询合约作品 我们要更新的下一个页面是**pages/index.js**。这是应用程序的主要入口,也会作为查询呈现待售数字资产的页面。 > 代码在[这里](https://gist.github.com/dabit3/3078acab4b3b831fd4a392844d860157) ```javascript /* 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 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 mb-4 font-bold text-white">{nft.price} ETH</p> <button className="w-full bg-pink-500 text-white font-bold py-2 px-12 rounded" onClick={() => buyNft(nft)}>Buy</button> </div> </div> )) } </div> </div> </div> ) } ``` 当页面加载时,我们查询智能合约中任何仍在销售的作品,并将它们与作品的元数据和购买按钮一起呈现在页面上。 #### 创建和上架作品 接下来,我们创建允许用户创建和上架作品的页面。 在这个页面上要做几件事情: 1. 用户能够上传和保存文件到IPFS 2. 用户能够创建一个新的独特的 NFT。 3. 用户能够设置作品的元数据和价格,并在市场上上架销售。 在用户创建并上架作品后,他们会被重新引导到主页面,以查看所有出售的作品。 > 代码在[这里](https://gist.github.com/dabit3/e3d24ec9fa22f10acecbfa75eb1e4284) ```javascript /* 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 ( <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={createMarket} className="font-bold mt-4 bg-pink-500 text-white rounded p-4 shadow-lg"> Create Digital Asset </button> </div> </div> ) } ``` #### 查看用户购买的作品 在**Market.sol**智能合约中,我们创建了一个名为`fetchMyNFT`的函数,只返回用户拥有的作品。 在**pages/my-assets.js**中,我们将使用该函数来获取和渲染它们。 这个功能与查询**pages/index.js**不同,因为需要获得用户的地址,并在合约中使用进行查询: > 代码在[这里](https://gist.github.com/dabit3/3062d11868124eceb5eb12e2d70609aa) ```javascript /* 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 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> </div> </div> )) } </div> </div> </div> ) } ``` #### 创建者仪表板 我们将创建的最后一个页面是创建者仪表板,将允许他们查看他们所创建的所有作品以及他们所销售的作品。 这个页面将使用**Market.sol**智能合约中的 `fetchItemsCreated `函数,它只返回与调用函数的用户地址相匹配的作品。 在客户端,我们使用`sold`布尔值将作品过滤到另一个单独的数组中,只向用户显示已经售出的作品。 在**pages**目录下创建一个名为**creator-dashboard.js**的新文件,代码如下: > 代码在[这里](https://gist.github.com/dabit3/feea5e27010e298cfeb16e80ba76850f) ```javascript /* 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 created</h1>) return ( <div> <div className="p-4"> <h2 className="text-2xl py-2">Items Created</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 className="px-4"> { Boolean(sold.length) && ( <div> <h2 className="text-2xl py-2">Items sold</h2> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 pt-4"> { sold.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> </div> ) } ``` ### 运行项目 为了运行该项目,我们需要有一个部署脚本,将智能合约部署到区块链网络。 #### 将合约部署到本地网络上 当我们创建该项目时,Hardhat在**scripts/sample-script.js**创建了一个部署脚本的例子。 为了使这个脚本的目的更加明确,将**scripts/sample-script.js**的名字更新为**scripts/deploy.js**。 接下来,用以下代码更新**scripts/deploy.js**中的`main`函数。 ```javascript 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 测试网](https://docs.matic.network/docs/develop/network-details/network/)。 打开终端并运行以下命令,可以启动一个本地网络: ``` npx hardhat node ``` 它创建了一个有19个账户的本地网络: ![Hardhat节点](https://img.learnblockchain.cn/pics/20210729090859.png) 接下来,保持节点运行,打开一个单独的终端窗口来部署合约,运行以下命令: ```bash npx hardhat run scripts/deploy.js --network localhost ``` 当部署完成后,CLI应该打印出被部署的合约的地址: ![合约地址](https://img.learnblockchain.cn/pics/20210729090855.png) 使用这些地址,在项目根部创建一个名为**config.js**的新文件,并添加以下代码,将占位符替换为CLI打印出的合约地址。 ``` export const nftmarketaddress = "nft-contract-address" export const nftaddress = "market-contract-address" ``` #### 将账户导入MetaMask 你可以将节点创建的账户导入你的Metamask钱包,在应用中进行使用,要导入这些账户之一,首先将你的MetaMask钱包网络切换到Localhost 8545。 ![Localhost网络](https://img.learnblockchain.cn/pics/20210729090848.jpeg) 接下来,在MetaMask中点击账户菜单中的**导入账户**: ![MetaMask导入账户](https://img.learnblockchain.cn/pics/20210729090844.jpeg) 复制并粘贴一个由CLI 打印出的**私钥(private keys)**,然后点击**导入(import)**。一旦账户被导入,你应该看到账户中的一些Eth。 ![MetaMask account](https://img.learnblockchain.cn/pics/20210729090839.jpeg) 我建议导入 2到3个账户,这样你就可以在用户之间测试各种功能了。 #### 运行应用程序 要启动该应用程序,在你的CLI中运行以下命令: ``` npm run dev ``` 测试一下,尝试上架一个作品进行销售,然后切换到另一个账户并购买它。 ## 部署到Polygon 现在我们已经运行了项目,并在本地进行了测试,让我们把它部署到Polygon。我们将首先部署到[Mumbai](https://docs.matic.network/docs/develop/network-details/network/#mumbai-testnet),即Matic测试网络。 我们需要做的第一件事是将我们钱包中的一个**私钥**保存到**.secrets**文件。 为了获得私钥,你可以使用Hardhat给你的一个私钥,或者你可以直接从MetaMask导出: ![私钥](https://img.learnblockchain.cn/pics/20210729090834.png) > 对于生产环境中的应用,我建议不要在代码中硬编码你的私钥,而是将其设置为类似环境变量的东西。 ### 配置网络 接下来,我们需要从本地测试网络切换到[Mumbai Testnet](https://docs.matic.network/docs/develop/network-details/network/#mumbai-testnet)。 要做到这一点,我们需要创建和设置网络配置。 首先,打开MetaMask,点击设置: ![MetaMask设置](https://img.learnblockchain.cn/pics/20210729090825.png) 接下来,点击**网络**,然后点击**添加网络**: ![新网络](https://img.learnblockchain.cn/pics/20210729090830.png) 在这里,我们将为孟买测试网络添加以下配置,如[这里](https://docs.matic.network/docs/develop/network-details/network/#mumbai-testnet)所列。 网络名称: **Mumbai 测试网** 新的RPC网址: **[https://rpc-mumbai.matic.today](https://rpc-mumbai.matic.today/)** 链ID: **80001** 货币符号: **Matic** 点保存,然后你应该能够切换到并使用新的网络! 最后,你将需要一些Testnet Matic代币,以便与应用程序交互。 要获得这些,你可以访问[Matic Faucet](https://faucet.matic.network/),输入你想申请代币的钱包地址。 ### 部署到Matic/Polygon网络上 现在你有了一些Matic代币,你就可以部署到Polygon网络了 要做到这一点,要确保与你部署合约的私钥相关的地址已经收到一些Matic代币,以支付交易的Gas费用。 运行以下命令部署到Matic : ``` npx hardhat run scripts/deploy.js --network mumbai ``` 一旦合约部署完毕,你应该可以在你的项目中更新合约地址,并在新的网络上进行测试 ! ``` npm run dev ``` #### 部署到主网 要部署到主Matic/Polygon网络,你可以使用我们为Mumbai测试网络设置的相同步骤。 主要区别在于,其节点不一样,参考[这里](https://docs.matic.network/docs/develop/network-details/network/),以及将网络导入你的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}` ``` > 项目的最终源代码,请访问[这个代码库](https://github.com/dabit3/polygon-ethereum-nextjs-marketplace/) ### 接下来 祝贺你!你已经在Polygon上部署了一个不复杂的应用程序。 与Polygon这样的解决方案合作,最酷的事情是,与直接在以太坊上构建相比,额外工作或学习很少。在这些第二层和侧链中,几乎所有的API和工具都是一样的,这使得任何技能都可以在像Polygon这样的不同平台上转移。 接下来,我建议使用[The Graph](https://thegraph.com/)来移植这个应用程序中实现的查询。图形将开辟更多的数据访问模式,包括分页、过滤和排序等对任何现实世界的应用都是必要的。 我还将在未来几周内发布一个教程,展示如何使用Polygon和The Graph。 --- 本翻译由 [CellETF](https://celletf.io/?utm_souce=learnblockchain) 赞助支持。

  • 原文:https://dev.to/dabit3/building-scalable-full-stack-apps-on-ethereum-with-polygon-2cfb
  • 译文出自:登链翻译计划
  • 译者:翻译小组
  • 校对:Tiny 熊
  • 本文永久链接:learnblockchain.cn/article…

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

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

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

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

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

安装依赖

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

  1. 在你的机器上安装Node.js
  2. 安装浏览器钱包插件Metamask

技术栈

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

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

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

关于项目

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

数字市场规则如下

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

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

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

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

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

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

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

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

关于Polygon

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

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

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

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

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

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

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

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

项目设置

我们将创建一个新的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进行样式设计,Tailwind是一个非常实用的CSS框架,它使创建优美好看的网站变得容易,我们安装Tailwind的依赖:

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

接下来,我们将通过运行以下命令,创建Tailwind与Next.js工作所需的配置文件(tailwind.config.jspostcss.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: &lt;Choose default path>

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

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

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

代码在这里

/* 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测试网

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

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

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

智能合约

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

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

代码在这里

// 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实现的ERC721标准

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

代码在这里

// 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);
    for (uint i = 0; i &lt; 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 &lt; totalItemCount; i++) {
      if (idToMarketItem[i + 1].owner == msg.sender) {
        itemCount += 1;
      }
    }

    MarketItem[] memory items = new MarketItem[](itemCount);
    for (uint i = 0; i &lt; 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 &lt; totalItemCount; i++) {
      if (idToMarketItem[i + 1].seller == msg.sender) {
        itemCount += 1;
      }
    }

    MarketItem[] memory items = new MarketItem[](itemCount);
    for (uint i = 0; i &lt; 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,用以下代码更新它:

代码在这里

/* 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进行测试:

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

构建前端

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

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

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

代码在这里

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

function MyApp({ Component, pageProps }) {
  return (
    &lt;div>
      &lt;nav className="border-b p-6">
        &lt;p className="text-4xl font-bold">Metaverse Marketplace&lt;/p>
        &lt;div className="flex mt-4">
          &lt;Link href="/">
            &lt;a className="mr-4 text-pink-500">
              Home
            &lt;/a>
          &lt;/Link>
          &lt;Link href="/create-item">
            &lt;a className="mr-6 text-pink-500">
              Sell Digital Asset
            &lt;/a>
          &lt;/Link>
          &lt;Link href="/my-assets">
            &lt;a className="mr-6 text-pink-500">
              My Digital Assets
            &lt;/a>
          &lt;/Link>
          &lt;Link href="/creator-dashboard">
            &lt;a className="mr-6 text-pink-500">
              Creator Dashboard
            &lt;/a>
          &lt;/Link>
        &lt;/div>
      &lt;/nav>
      &lt;Component {...pageProps} />
    &lt;/div>
  )
}

export default MyApp

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

查询合约作品

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

代码在这里

/* 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 (&lt;h1 className="px-20 py-10 text-3xl">No items in marketplace&lt;/h1>)
  return (
    &lt;div className="flex justify-center">
      &lt;div className="px-4" style={{ maxWidth: '1600px' }}>
        &lt;div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 pt-4">
          {
            nfts.map((nft, i) => (
              &lt;div key={i} className="border shadow rounded-xl overflow-hidden">
                &lt;img src={nft.image} />
                &lt;div className="p-4">
                  &lt;p style={{ height: '64px' }} className="text-2xl font-semibold">{nft.name}&lt;/p>
                  &lt;div style={{ height: '70px', overflow: 'hidden' }}>
                    &lt;p className="text-gray-400">{nft.description}&lt;/p>
                  &lt;/div>
                &lt;/div>
                &lt;div className="p-4 bg-black">
                  &lt;p className="text-2xl mb-4 font-bold text-white">{nft.price} ETH&lt;/p>
                  &lt;button className="w-full bg-pink-500 text-white font-bold py-2 px-12 rounded" onClick={() => buyNft(nft)}>Buy&lt;/button>
                &lt;/div>
              &lt;/div>
            ))
          }
        &lt;/div>
      &lt;/div>
    &lt;/div>
  )
}

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

创建和上架作品

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

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

  1. 用户能够上传和保存文件到IPFS
  2. 用户能够创建一个新的独特的 NFT。
  3. 用户能够设置作品的元数据和价格,并在市场上上架销售。

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

代码在这里

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

查看用户购买的作品

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

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

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

代码在这里

/* 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 (&lt;h1 className="py-10 px-20 text-3xl">No assets owned&lt;/h1>)
  return (
    &lt;div className="flex justify-center">
      &lt;div className="p-4">
        &lt;div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 pt-4">
          {
            nfts.map((nft, i) => (
              &lt;div key={i} className="border shadow rounded-xl overflow-hidden">
                &lt;img src={nft.image} className="rounded" />
                &lt;div className="p-4 bg-black">
                  &lt;p className="text-2xl font-bold text-white">Price - {nft.price} Eth&lt;/p>
                &lt;/div>
              &lt;/div>
            ))
          }
        &lt;/div>
      &lt;/div>
    &lt;/div>
  )
}

创建者仪表板

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

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

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

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

代码在这里

/* 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 (&lt;h1 className="py-10 px-20 text-3xl">No assets created&lt;/h1>)
  return (
    &lt;div>
      &lt;div className="p-4">
        &lt;h2 className="text-2xl py-2">Items Created&lt;/h2>
          &lt;div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 pt-4">
          {
            nfts.map((nft, i) => (
              &lt;div key={i} className="border shadow rounded-xl overflow-hidden">
                &lt;img src={nft.image} className="rounded" />
                &lt;div className="p-4 bg-black">
                  &lt;p className="text-2xl font-bold text-white">Price - {nft.price} Eth&lt;/p>
                &lt;/div>
              &lt;/div>
            ))
          }
        &lt;/div>
      &lt;/div>
        &lt;div className="px-4">
        {
          Boolean(sold.length) && (
            &lt;div>
              &lt;h2 className="text-2xl py-2">Items sold&lt;/h2>
              &lt;div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 pt-4">
                {
                  sold.map((nft, i) => (
                    &lt;div key={i} className="border shadow rounded-xl overflow-hidden">
                      &lt;img src={nft.image} className="rounded" />
                      &lt;div className="p-4 bg-black">
                        &lt;p className="text-2xl font-bold text-white">Price - {nft.price} Eth&lt;/p>
                      &lt;/div>
                    &lt;/div>
                  ))
                }
              &lt;/div>
            &lt;/div>
          )
        }
        &lt;/div>
    &lt;/div>
  )
}

运行项目

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

将合约部署到本地网络上

当我们创建该项目时,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 测试网。

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

npx hardhat node

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

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

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

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

使用这些地址,在项目根部创建一个名为config.js的新文件,并添加以下代码,将占位符替换为CLI打印出的合约地址。

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

将账户导入MetaMask

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

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

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

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

运行应用程序

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

npm run dev

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

部署到Polygon

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

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

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

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

配置网络

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

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

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

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

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

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

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

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

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

部署到Matic/Polygon网络上

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

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

运行以下命令部署到Matic :

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

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

npm run dev

部署到主网

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

主要区别在于,其节点不一样,参考这里,以及将网络导入你的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}`

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

接下来

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

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

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

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

本翻译由 CellETF 赞助支持。

区块链技术网。

  • 发表于 2021-07-31 15:04
  • 阅读 ( 2717 )
  • 学分 ( 61 )
  • 分类:NFT

评论