使用 Truffle、IPFS、OpenSea 和 Polygon 部署 NFT
自我介绍。我的第一篇帖子
您好,我从小就学习信息技术(现在21岁),这是我的职业。过去18个月里,学习JS和Solidity对我来说非常理想。考虑到我之前两年一直在一些我完全不感兴趣的行业里摸索,编程对我的成年生活帮助极大。它赋予了我生活的意义,让我找到了生活的重心,也教会了我如何安排自己的生活。2018年大学毕业后,我虽然完成了IT专业的课程,但对未来却一无所知,感到非常迷茫。从小到大,我对任何比编程更复杂的东西都感到恐惧。我对学习新事物缺乏好奇心,而且总是担心别人对我的成功或失败的看法。慢慢地,我走出了困境。这需要我更加自省。我从最基础的小事做起(比如helloWorld.js、在控制台打印等等),循序渐进地学习技能,这绝对是正确的学习方法。耐心是关键。我拥有 React.js、Express.js 和 Solidity 方面的经验,我的目标是成为一名全栈区块链应用开发人员。
旧的 GitHub 仓库
Polygon 上的新 Hardhat 仓库,包含 MetaTXs
一个支持 MetaTX 和 ERC1155 的全新 Hardhat 仓库
在这里,您将学习如何将简单的 ERC-721 合约部署到孟买测试网,以及它是如何工作的。
您可以随意跳过本指南,只选择您需要的部分。不过,这并非那种“先做这个,再做那个”的教程(或者其他类似的套路)。我希望解释代码的运行机制,帮助您掌握一些知识,尽管使用的是一些基础工具。请做好阅读冗长内容的准备。
本教程的灵感来源于Opensea和Openzeppelin 的 ERC-721 教程。我计划编写自己的教程,提供更详细的信息和更流畅的指导。你可以直接使用 Opensea 的前端进行部署和挖矿,但本教程旨在为开发者提供一个基本的起点,帮助他们在此基础上构建项目构想。
我发现以下资源对于深入了解 Solidity 非常有用:CryptoDevHub提供指导,Solidity By Example提供代码示例。
本项目使用Truffle开发框架,因为它易于上手,并且能让你体验 Node.js 环境。社区更倾向于使用 Hardhat。然而,我目前对 Hardhat 的了解还不够深入。就本教程而言,Truffle 已经足够,但我计划在未来推出 Hardhat 教程。如果你对 Solidity 一无所知,可以试试Remix,它是一款更快捷、更小巧的浏览器开发工具,可以帮助你练习 Solidity 的基础知识。
我的下一个教程将重点介绍ERC-1155,它是 ERC-721 的改进版。这个新标准允许开发者在一个合约中拥有多个同质化和非同质化代币的集合,从而减少 gas 费用。
令牌元数据存储
我将使用 Pinata 来进行快速简便的 IPFS 上传。Pinata 是一项第三方服务。不过,将来我建议您使用IPFS 桌面版,这样您就可以在树莓派或其他设备上运行节点。撰写本文时,IPFS 网络的性能一直不稳定。托管 IPFS 节点是更专业的做法,但这超出了本教程的范围。您或许会对Arweave这样的替代方案感兴趣。Arweave 与 IPFS 类似,都是分布式网络。但是,Arweave 采用的是Blockweave架构,更类似于区块链。简而言之,您可以支付 gas 费用来存储不可变文件。
让我们先来看看你需要准备哪些先决条件,以及一些链接,方便你了解更多将要使用的工具。具备一定的编程基础会有很大帮助。我使用的是Windows 10 系统,使用的是默认的 cmd 命令,而不是 ps 命令,但 macOS 和大多数 Linux 发行版应该也能满足本教程的要求。
先决条件
- 具备以太坊区块链的基本/中级知识(从应用开发者的角度出发)
- Solidity 基础知识,Solidity 是以太坊合约最流行的编程语言。这包括由OpenZeppelin提供的、可供合约扩展的可信 ERC 接口。
- 操作系统上已安装Node.js。具备中级JavaScript知识,尤其精通Node.js。
- Truffle CLI 已安装在您的操作系统上。Truffle 开发工具基础知识
- IPFS基础知识
- VS Code或其他文本编辑器 IDE,例如 Atom、Sublime Text 或 Eclipse
- MetaMask 浏览器扩展程序,已配置 Polygon Mumbai 测试网。您可以在其他/新的浏览器上安装此扩展程序时创建一个新的钱包,以避免使用您的主钱包。
- 用于克隆仓库的 Git 命令行工具。
- 在Pinata创建账户,即可获得 1GB 的元数据存储空间。
- 在MaticVigil创建一个帐户,即可获得多边形(原名 matic)节点 URL。这是最简单的第三方多边形节点。
链接
- 以太坊开发者文档
- 固体
- OpenZeppelin 的 ERC-721 文档
- JS 文档
- Node.js
- 松露文档
- IPFS 文档+ Pinata
- VS Code 下载
- Metamask
- 在 Metamask 上配置 Matic 网络
- Git
如果你没有这些软件,或者一个都没有,我建议先安装 Node.js,然后再安装其他软件(Truffle 框架使用 Node.js,需要 Node.js 才能安装)。安装好 Node.js 后,你就可以npm i -g truffle在一个新的终端窗口中输入命令了。
设置环境
你可以开始git在控制台中输入命令。然后,node也检查一下。接下来,启动 VS Code,创建一个新的工作区或文件夹。在该文件夹中打开一个新的终端,并使用`npmgit clone https://github.com/YourNewEmpire/Truffle-Tutorial-ERC721 clone` 命令克隆仓库。克隆完成后,输入`npm npm installinstall` 安装本项目所需的所有模块,这里用到了基本的 npm 功能。
在项目根目录下创建“.env”文件。该文件将保存您的钱包助记词/助记符以及脚本所需的免费 Matic 节点 URL。将以下内容写入该文件。
MNEMONIC=
NODE_KEY=
钱包
首先,我们来配置 MetaMask 连接到孟买测试网。点击此处查看 Matic/Mumbai 的网络详情。选择孟买测试网,复制其名称、链 ID、任意一个 RPC URL 以及货币代码 MATIC。然后将它们输入到 MetaMask 中。 这样做可能比较麻烦,因为 MetaMask 会在您再次访问该网站时关闭。手动输入可能更方便。
让我们获取您的钱包助记词,以便将其添加到环境变量中,从而可以发送交易。点击右上角的头像,然后点击设置。

向下滚动找到“安全与隐私”。然后,点击红色的“显示种子”按钮,并确保您的密码已准备就绪。
复制助记词,将其粘贴到你的 .env 文件中MNEMONIC=private key here。现在你可以在部署和挖矿脚本中使用此变量。它们已经通过模板字面量绑定到 JS 脚本中。
你需要使用你的以太坊地址来运行 web3 脚本,以便为自己铸造代币。从这里复制你的地址, 然后转到你的项目中。最后,将你的地址粘贴到第 14 行。
truffle-tutorial\scripts\mint.jsconst OWNER_ADDRESS = "HERE with the string quotes"
前往Matic Faucet页面,
Matic/Mumbai 节点网址。
在MaticVigil创建账户后,您将获得一个专用的 rpc url 密钥,以便顺利进行合约交互。
登录并进入控制面板后,您可以创建一个新应用,该应用应该会立即显示在您的控制面板上。
将其粘贴到您的 .env 文件中NODE_URL=key。
好的。环境设置完毕后,我们接下来准备代币铸造时的代币元数据。
Opensea 的元数据格式
首先,让我们来看看合约层面的 NFT 铸造。
function mintItem(address player, string memory tokenURI)
public
onlyOwner
returns (uint256)
{
_tokenIds.increment();
uint256 newItemId = _tokenIds.current();
_mint(player, newItemId);
_setTokenURI(newItemId, tokenURI);
return newItemId;
}
mint 函数接受两个参数。其中最重要的参数是 tokenURI。mintItem 函数调用该_setTokenURI函数,该函数接收刚刚分配的 id(索引)和URI。URI只是指向 token 资源的链接。在我们的例子中,它位于 IPFS 上。
关键在于 Opensea 将通过tokenURIERC-721 标准接口的一部分来查询该 tokenURI。
您还可以设置一个 baseURI,正如 OZ 文档中提到的那样。这将作为代币 URI 的前缀,这样当您铸造或查询 NFT 时,只需传递 ID,前缀即可完成 URI 格式的其余部分。
那么,Opensea 如何处理获取到的 URI 资源呢?这就需要用到 Opensea 的元数据标准了。Opensea 必须遵循这些标准才能读取和显示您的元数据,您肯定不希望 NFT 图片是空白的。
每个令牌 URI 都将是一个 JSON 文件,格式大致如下:
{
"description": "Friendly OpenSea Creature that enjoys long swims in the ocean.",
"external_url": "https://openseacreatures.io/3",
"image": "https://storage.googleapis.com/opensea-prod.appspot.com/puffs/3.png",
"name": "Dave Starbelly",
"attributes": [ ... ],
}
Opensea 特别需要包含上述名称的名称,例如“image”、“name”等。
我将使用名称、描述和图像。如果您想尝试音频和视频,动画 URL就是我上面链接的标准页面中提到的那个。图像或动画 URL 将指向 IPFS 上的另一个资源。这才是 Opensea 上会显示的图像或视频。
除了 NFT 元数据之外,还有从 GameItem.sol 合约中的函数获取的CollectioncontractUri()元数据。每个 NFT 合约都是一个 Collection。合约上铸造的每个代币都是一个 NFT。我们需要上传 Collection 的元数据,格式包括图像、名称和描述。
创建并上传令牌元数据
既然我们已经了解了 NFT 元数据的相关指南,那么在部署任何合约之前,我们需要编写一些文档。由于使用了 IPFS,我决定整理我的文件夹,将收藏数据与 NFT 数据分开。我之所以采用这种方式,是因为我通常会铸造多个 NFT,因此我想将我的媒体文件上传到不同的文件夹中。您可以根据自己的喜好随意整理。
首先在项目根目录下创建两个文件夹:collection-data 和 nft-data。如果您打算将项目推送到 GitHub,建议您将这两个文件夹添加到 .gitignore 文件中。在 collection-data 文件夹中,放入您的集合图像和 json 文件,格式如下:
当然,您可以使用自己的值。`external_link` 参数并非至关重要,但值得测试。例如,如果您将 `name` 留空,Opensea 会自动生成一个名称。
您可能已经注意到,图片值为空。我们需要先上传图片集,才能获取图片 URI,以便 JSON 文件指向它,接下来我们将上传该 JSON 文件。尽管图片和 JSON 文件位于项目文件夹中,但必须先上传图片,再上传 JSON 文件。
准备好测试镜像后,请按照先决条件打开 Pinata IPFS 并上传。登录后,您将直接进入上传页面。点击“上传文件”,然后在文件资源管理器中选择项目中的镜像文件。
在您的控制面板中,您会看到该文件有一个 CID(内容标识符)。正如IPFS 文档中所述,CID 是基于内容的地址/哈希值,而非实际位置。这对我们来说并非至关重要,但仍然值得关注。
点击图片将跳转到 Pinata 网关 URL。我使用这种 URI 格式体验最佳:https://ipfs.io/ipfs/your-collection-cid。复制此 URL 和您在 Pinata 中获取的 CID,并将其粘贴到项目中的集合 JSON 文件中:
{
"name": "Collection Name",
"description": "A test collection for ERC721 tutorial",
"image":
"https://ipfs.io/ipfs/QmQ2CPtFwRxoASHmiqbTPSfuPr91NfbMw2fjPtgvz55LPL"
}
您也可以对 NFT 数据执行相同的操作。首先上传测试 NFT 镜像,然后将 cid 复制粘贴到 'nft.json' 文件中。JSON 文件可能如下所示:
{
"name": "Example Coffee #1",
"description": "I am a coffee but, this .jpg extension allows me to be warm forever.",
"image": "https://ipfs.io/ipfs/QmSXSHaUDJTorFReSJA24HjruRo2yqYbxPUSbxG7xTi31c"
}
最后,您可以上传集合和 NFT 的 JSON 文件。铸造多个NFT 时,我喜欢将所有媒体文件上传到一个文件夹中,这样 URI 看起来会像这样:https://ipfs.io/ipfs/folder-hash/nft1.jpg。不过,本文中我将上传 1 张 JPG 图片。
至此,元数据部分就完成了。接下来,我们将进行部署和铸造,届时您需要用到集合和 NFT 的 JSON CID,想想就令人兴奋!
部署和铸造 NFT
元数据和环境都设置好之后,我们可以开始这个阶段了,首先查看项目 contracts 文件夹中的 GameItem.sol 文件。它看起来会像这样:
pragma solidity ^0.8.0;
import "../node_modules/@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "../node_modules/@openzeppelin/contracts/utils/Counters.sol";
import "../node_modules/@openzeppelin/contracts/access/Ownable.sol";
contract GameItem is ERC721URIStorage, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
// set contract name and ticker.
constructor() ERC721("Contract Name", "TIC") {}
//get the current supply of tokens
function totalSupply() public view returns (uint256) {
return _tokenIds.current();
}
// for opensea collection
function contractURI() public pure returns (string memory) {
return "https://ipfs.io/ipfs/your-collection-ipfshash";
}
function mintItem(address player, string memory tokenURI)
public
onlyOwner
returns (uint256)
{
_tokenIds.increment();
uint256 newItemId = _tokenIds.current();
_mint(player, newItemId);
_setTokenURI(newItemId, tokenURI);
return newItemId;
}
}
如果您是 Solidity 新手,这里有一些需要讲解的内容。首先是 pragma 指令。pragma 指令的作用是设置编译文件时使用的 Solidity 编译器版本。本项目使用的是 0.8.0 版本,这是 Solidity 可用的最高次要版本,可以在 truffle-config.js 文件中进行设置。
接下来,我们导入 Ownable,用于仅对所有者(即您,部署者)修改函数调用;然后导入 Counters 来存储 tokenId,这是一个用于安全跟踪令牌的可信工具。ERC721URIStorage 继承了经典的 ERC721 接口,并为合约添加了一些令牌 URI 辅助功能,这些功能最近已在 Openzeppelin 4.x 版本中添加。您绝对应该了解一下这些库和抽象合约,因为它们是构建您自己的接口的绝佳示例。
对我们来说,最重要的部分是 contractURI 函数。正如我之前所说,一个 ERC-721 合约对应一个集合。此函数返回一个字符串,即指向您的集合 JSON 的 URI。Opensea 在其前端显示您的集合时会调用此方法。
将您的集合 JSON ipfs URI 复制到返回语句中。
function contractURI() public pure returns (string memory) {
return "https://ipfs.io/ipfs/your-collection-ipfshash";
}
您也可以选择在构造函数中设置代币代码和代币/合约名称。此函数在部署时运行一次,用于初始化某些内容。在本例中,它通过 ERC721URIStorage 继承了 ERC-721 协议,并需要代币代码和名称参数。这是一个测试,因此我将保留默认值。
请确保您的钱包短语和节点 URL 已添加到 .env 文件中,这样我们就可以部署此合约了。在项目窗口中打开一个新的终端并输入命令truffle develop。PowerShell 可能需要npx前缀。此命令将为部署、编译等操作准备好 truffle CLI。您可以输入命令compile进行单次编译而不进行部署,但部署时 truffle 仍然会进行编译。
要在终端中部署,请运行命令migrate --network mumbai。**您可能会遇到错误**,尤其是在某些特定环境下。这些工具并非完美无缺。一个好的故障排除方法是运行 `migrate --network development` 命令来找出错误所在。
我收到了这个:
Starting migrations...
======================
> Network name: 'mumbai'
> Network id: 80001
> Block gas limit: 20000000 (0x1312d00)
1_initial_migration.js
======================
Replacing 'Migrations'
----------------------
> transaction hash: 0x4f703c7184a36b92af5fdd5d7751a7ed444670031475dfc90009927b96949d82
> Blocks: 2 Seconds: 8
> contract address: 0xb6e5A1B174C1CA435cB43Cf61fF9064F87f5f0Ec
> block number: 18792256
> block timestamp: 1631363185
> account: 0x5f4c3843495Babe89cB3516cEbD8840024e741fa
> balance: 1.408520183748380055
> gas used: 245600 (0x3bf60)
> gas price: 3 gwei
> value sent: 0 ETH
> total cost: 0.0007368 ETH
Pausing for 2 confirmations...
------------------------------
> confirmation number: 2 (block: 18792258)
> Saving migration to chain.
> Saving artifacts
-------------------------------------
> Total cost: 0.0007368 ETH
2_deploy_token.js
=================
Replacing 'GameItem'
--------------------
> transaction hash: 0x2a0bc70f5c77c9c28e4a237de7adf72bac55c5d05d744a013c1dbd67fd1f245b
> Blocks: 2 Seconds: 4
> contract address: 0x87E67eBEBb785060d4Ed85Bff7E67dEc9Efa87F4
> block number: 18792264
> block timestamp: 1631363201
> account: 0x5f4c3843495Babe89cB3516cEbD8840024e741fa
> balance: 1.400152706748380055
> gas used: 2743246 (0x29dbce)
> gas price: 3 gwei
> value sent: 0 ETH
> total cost: 0.008229738 ETH
Pausing for 2 confirmations...
------------------------------
> confirmation number: 2 (block: 18792266)
> Saving migration to chain.
> Saving artifacts
-------------------------------------
> Total cost: 0.008229738 ETH
Summary
=======
> Total deployments: 2
> Final cost: 0.008966538 ETH
- Blocks: 0 Seconds: 0
- Saving migration to chain.
- Blocks: 0 Seconds: 0
- Saving migration to chain.
请不要关闭终端,我们还没完成。如您所见,Truffle 运行了迁移脚本来部署 Migrations.sol 合约,之后是代币脚本。这是 Truffle 部署合约的方式。您可以通过在项目的 migrations 文件夹中创建更多 js 脚本来执行更多部署。如果您的部署成功,恭喜您,您的合约已成功部署。
我们需要代币合约地址来进行铸造,您可以从孟买 Tesnet区块浏览器(Mumbai Tesnet Block Explorer)中获取,地址位于您的地址下。方便的是,Truffle 在将代币部署到区块链后,已将该地址打印到控制台。
contract address: your token address从控制台的 2_deploy_token.js 日志中复制该行内容。将其粘贴到 scripts/mint.js 文件的第 13 行,如下所示:
const NFT_CONTRACT_ADDRESS = "0x87E67eBEBb785060d4Ed85Bff7E67dEc9Efa87F4"
web3 脚本需要这个常量变量来实例化合约,以便调用/发送合约上的方法。
脚本还需要 OWNER_ADDRESS 变量中的您的账户地址才能向您铸造硬币,并且需要您发送此交易。
您可能会注意到,我们使用 fs 读取合约工件 JSON:
let rawdata = fs.readFileSync(path.resolve(__dirname, "../build/contracts/GameItem.json"));
let contractAbi = JSON.parse(rawdata);
const NFT_ABI = contractAbi.abi
可能需要针对 Linux 和 macOS 进行修改。我对 Linux 文件系统不是很熟悉。这段脚本的作用是从项目中的另一个文件中读取合约 ABI。
Web3.js 在调用/发送时需要此 JSON 作为参考。因此,如果您在同一个项目中编译了一个完全不同的合约,将会覆盖现有文件。完成此合约后,请为新合约创建一个新项目。
本脚本的最后一个要求是,你需要将之前上传到 Pinata IPFS 的 NFT JSON 的 CID 哈希值作为 mintItem 函数的参数。请将其粘贴到脚本的第 43 行:
await nftContract.methods
.mintItem(OWNER_ADDRESS, `https://ipfs.io/ipfs/your-tokenjson-cid`)
.send({ from: OWNER_ADDRESS })
.then(console.log('minted'))
.catch(error => console.log(error));
脚本准备就绪后,node scripts/mint.js在项目终端中运行。您可以打开一个新的终端来执行此操作,或者按 CTRL+C 退出当前终端中的 truffle 命令行界面。
如果没有错误,控制台应该会显示“已铸造”,你可以在区块浏览器上查看你的账户,确认该区块已被铸造。如果发现任何错误,请在评论区留言并自行搜索相关信息。
在浏览器中登录 Metamask 后,让我们通过现有合约在 Opensea 上查看我们的 NFT。前往Opensea 测试网前端。打开个人资料图标下拉菜单登录,然后点击“我的收藏”:
接下来,点击“创建集合”旁边的三个点菜单图标,然后选择“导入现有合约”。之后系统会询问该 NFT 是在主网还是测试网上。当然,请选择测试网。
现在您可以将您的 NFT 合约地址输入到该字段中,并在左侧的下拉菜单中选择“孟买”,如下所示:
您可能会收到以下消息:“我们找不到此合约。请确保这是部署在孟买的有效 ERC721 或 ERC1155 合约,并且您已在该合约上铸造了物品。”
这是一个常见问题,因为 Opensea 会在条件允许的情况下显示您的代币。只要您能在 Polygon Scan 上看到代币合约和交易记录,就说明这是一个有效的 ERC-721 合约,并且已经铸造了 1 个代币。有些开发者甚至等待了 24 小时以上才看到他们的 NFT 显示出来。
这是我的 NFT。第一次尝试时,我犯了个错误,没有将集合哈希添加到 contractURI 函数中。尽管我已修复此问题并重新部署了合约,但 Opensea 仍然无法读取集合元数据。幸运的是,您可以通过编辑集合来更改此设置。至少我的“示例咖啡”NFT 的元数据可以正常工作。
最后说明和结论
所有精彩瞬间都已过去,让我们来回顾一下。
你已经学会了一种将 ERC-721 代币部署到 Polygon 测试网的简单方法,不错。你可以对 Matic 主网重复此过程,前提是你的 Matic 主网余额中有 MATIC 代币,并且你需要编辑你的 mint 脚本,使其使用MATIC关键字而不是MUMBAImint.js 文件第 31 行进行实例化。
本教程还有一些我遗漏的内容。
首先,为了更专业,我可以验证合约。虽然在 Hardhat 中验证合约更容易,但我目前还没有 Hardhat 的可用示例。Truffle 验证合约需要更多配置,因此我将把这部分内容留到下一篇教程中讲解。
您还可以选择在合约中添加额外代码,以避免在 Opensea 出售物品时支付 Gas 费。Opensea文档中解释了具体操作方法。只需将 Opensea 的 Matic 合约地址设置为合约的运营商即可。这样,Opensea 就可以为您转移 NFT,从而为您或任何 NFT 所有者节省 Gas 费。
除了以上这些额外功能外,您可能还想了解一下在生产环境中冻结元数据的功能。这是 Opensea 提供的一项功能,可以阻止元数据被修改。NFT 的买家和卖家看到他们的元数据被冻结后会感到安心。
最后,我想通过以下链接引导您学习更多更好的教程:






