如何编写 NFT 智能合约

原文标题:Writing an NFT Collectible Smart Contract

原文作者:Rounak Banik

原文来源:dev.to

编译:登链翻译小组

简介

在之前的教程中,我们向你展示了如何使用我们的生成艺术库[4]来创建一个头像集合[5],生成符合要求的 NFT 元数据,并将元数据 JSON 和媒体文件上传至 IPFS[6]。

然而,我们还没有把头像铸成 NFT。因此,在本教程中,我们将编写一个智能合约,允许任何人通过支付 Gas 从我们的藏品中铸造一个 NFT。

前提

智能合约

  1. 了解 Javascript 的中级知识(如果你需要复习,我建议使用这个YouTube 教程[7])。
  2. 了解 Solidity 和 OpenZeppelin 合约的中级知识(推荐CryptoZombies[8]和Buildpace[9])。
  3. 在本地电脑上安装 node 和 npm
  4. 准备好一组媒体文件和 NFT 元数据 JSON 上传至 IPFS。(如果你没有这个,我们已经创建了一个玩具集供你实验。你可以在这里[10]找到媒体文件和在这里[11]找到 JSON 元数据文件)。

虽然不满足先决条件的读者可能会跟着做,甚至可以部署一个智能合约,但如果你对你的项目很认真,我们强烈建议找一个知道自己在做什么的开发者。智能合约的开发和部署可能是非常昂贵的,而且在安全缺陷和 bug 方面也不宽容。

设置本地开发环境

智能合约

我们将使用 Hardhat,一个行业标准的以太坊开发环境,来开发、部署和验证我们的智能合约。为项目创建一个空文件夹,并通过在终端运行以下命令初始化一个空 package.json 文件:

mkdir nft-collectible && cd nft-collectible && npm init -y

你现在应该在nft-collectible文件夹内,并有一个名为package.json的文件。

接下来,让我们安装 Hardhat。运行以下命令:

npm install –save-dev hardhat

现在我们可以通过运行以下命令并选择 “Create a basic sample project(创建一个基本样本项目)”来创建项目:

npx hardhat

同意所有的默认值(项目根目录,添加.gitignore,并安装所有样本项目的依赖项)。

让我们检查样本项目是否已经正确安装,运行以下命令:

npx hardhat run scripts/sample-script.js

如果一切顺利,你应该看到像这样的输出:

智能合约

我们现在已经成功地配置了 Hardhat 开发环境。现在安装 OpenZeppelin 合约包。这将使我们能够访问 ERC721 合约(NFT 的标准),以及一些我们以后会遇到的辅助库:

npm install @openzeppelin/contracts

如果我们要公开分享项目的代码(在 GitHub 这样的网站上),我们不想分享敏感信息,比如私钥、Etherscan API 密钥或我们的 Alchemy URL(如果其中一些词对你还没有意义,请不要担心)。因此,让我们安装另一个名为 dotenv 的库:

npm install dotenv

我们现在可以开始开发智能合约了。

编写智能合约

智能合约

在这一节中,我们将在Solidity[12]中编写一个智能合约,允许任何人通过支付所需数量的以太币+Gas 来铸造一定数量的 NFT。

在你项目的contracts文件夹中,创建一个名为NFTCollectible.sol的新文件。

我们将使用 Solidity v8.0,合约将继承 OpenZeppelin 的ERC721Enumerable和Ownable合约。前者有一个 ERC721(NFT)标准的默认实现,此外还有一些在处理 NFT 时有用的辅助函数。后者允许我们在合约的增加管理权限。

除了上述内容,还将使用 OpenZeppelin 的SafeMath和Counters库来分别安全地处理无符号整数运算(通过防止溢出)和 tokenID。

这就是我们合约的骨架,看起来像这样:

//SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import “@openzeppelin/contracts/utils/Counters.sol”;

import “@openzeppelin/contracts/access/Ownable.sol”;

import “@openzeppelin/contracts/utils/math/SafeMath.sol”;

import “@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol”;

contract NFTCollectible is ERC721Enumerable, Ownable {

using SafeMath for uint256;

using Counters for Counters.Counter;

Counters.Counter private _tokenIds;

}

常量和变量

我们的合约需要跟踪某些变量和常量。在本教程中,定义以下内容:

  1. 供应量(Supply):可以铸造的 NFT 的最大数量。
  2. 价格:购买 1 个 NFT 所需的以太币数量。
  3. 每次交易的最大铸币数量:你一次可以铸造的 NFT 的上限。
  4. 代币 URI 前缀(baseTokenURI):包含 JSON 元数据的文件夹的 IPFS URL。

在本教程中,我们将把 1-3 设置为常数。换句话说,一旦合约被部署,我们将无法修改它们。我们将为baseTokenURI编写一个 setter 函数,允许合约的所有者(或部署者)在需要时修改它。

在_tokenIds声明下,添加以下内容:

uint public constant MAX_SUPPLY = 100;

uint public constant PRICE = 0.01 ether;

uint public constant MAX_PER_MINT = 5;

string public baseTokenURI;

注意,常数使用了大写字母。请根据你的项目自由改变常数的值。

构造函数

我们将在构造函数的调用中设置baseTokenURI。还将调用父级构造函数并为 NFT 合约设置名称和符号。

因此,构造函数看起来像这样:

constructor(string memory baseURI) ERC721(“NFT Collectible”, “NFTC”) {

setBaseURI(baseURI);

}

保留 NFT 的功能

作为项目的创建者,你可能想为你自己、团队以及像赠品这样的活动保留一些 NFT 的集合。

让我们写一个函数,允许我们免费铸造一定数量的 NFT(在这里为 10 个)。由于调用这个函数的人只需要支付 Gas 费,显然需要把它标记为onlyOwner,这样只有合约的所有者才能调用它:

function reserveNFTs() public onlyOwner {

uint totalMinted = _tokenIds.current();

require(

totalMinted.add(10) < MAX_SUPPLY, “Not enough NFTs”

);

for (uint i = 0; i < 10; i++) {

_mintSingleNFT();

}

}

我们通过调用tokenIds.current()来检查到目前为止铸造的 NFT 的总数。然后检查是否有足够的 NFT 供我们保留。如果是,我们继续通过调用_mintSingleNFT10 次来铸造 10 个 NFT。

在_mintSingleNFT函数中,真正的魔法发生了。我们稍后将研究它。

设置 baseTokenURI

NFT JSON 元数据可以在这个 IPFS URL 上找到:ipfs://QmZbWNKJPAjxXuNFSEaksCJVd1M6DaKQViJBYPK2BdpDEP/。

当我们把这个设置为baseTokenURI时,OpenZeppelin 的实现会自动推导出每个 token 的 URI。它假定 token1 的元数据在ipfs://QmZbWNKJPAjxXuNFSEaksCJVd1M6DaKQViJBYPK2BdpDEP/1,代币 2 的元数据在ipfs://QmZbWNKJPAjxXuNFSEaksCJVd1M6DaKQViJBYPK2BdpDEP/2,等等

(请注意,这些文件没有.json扩展名)。

相应的函数是:

function _baseURI() internal

view

virtual

override

returns (string memory) {

return baseTokenURI;

}

function setBaseURI(string memory _baseTokenURI) public onlyOwner {

baseTokenURI = _baseTokenURI;

}

在合约部署之后,合约的所有者允许改变baseTokenURI。

Mint NFT 函数

现在让我们把注意力转向主要的 Mint NFT 函数。当用户和客户想从我们的收藏中购买和铸造 NFT 时,他们会调用这个函数。

由于他们要向这个函数发送以太币,我们必须将其标记为 payable.

在真实铸币发生之前,我们需要做三个检查:

  1. 有足够的 NFT 数量供调用者铸造。
  2. 请求的铸币数量超过 0,但少于每笔交易允许的最大 NFT 数量。
  3. 调用者已经发送了足够的以太币来铸造所要求的 NFT 数量。

function mintNFTs(uint _count) public payable {

uint totalMinted = _tokenIds.current();

require(

totalMinted.add(_count) <= MAX_SUPPLY, “Not enough NFTs!”

);

require(

_count > 0 && _count <= MAX_PER_MINT,

“Cannot mint specified number of NFTs.”

);

require(

msg.value >= PRICE.mul(_count),

“Not enough ether to purchase NFTs.”

);

for (uint i = 0; i < _count; i++) {

_mintSingleNFT();

}

}

铸造单个 NFT 函数

最后让我们看看私有的_mintSingleNFT()函数,每当我们(或第三方)想铸造一个 NFT 时,都会调用这个函数:

function _mintSingleNFT() private {

uint newTokenID = _tokenIds.current();

_safeMint(msg.sender, newTokenID);

_tokenIds.increment();

}

这里发生的事情:

  1. 得到当前还没有被铸造的 ID。
  2. 使用 OpenZeppelin 已经定义的_safeMint()函数,将 NFT ID 分配给调用该函数的账户。
  3. 我们将 tokenID 的计数器递增 1。

在发生任何铸币行为之前,代币 ID 为 0。

当这个函数第一次被调用时,newTokenID是 0。调用safeMint()将 ID 为 0 的 NFT 分配给调用合约函数的人,然后计数器被递增到 1。

下次调用此函数时,_newTokenID的值为 1。调用safeMint()将 ID 为 1 的 NFT 分配给……我想你能明白这个要点。

注意,我们不需要为每个 NFT 再次设置元数据。设置baseTokenURI可以确保每个 NFT 自动获得正确的元数据(存储在 IPFS 中)。

获取一个特定账户所拥有的所有代币

如果你打算给你的 NFT 持有人提供类似列表类的功能,你会想每个用户持有哪些 NFT。

让我们写一个简单的函数,返回一个特定持有人拥有的所有 ID。

ERC721Enumerable 的 “balanceOf “和 “tokenOfOwnerByIndex “函数使之变得超级简单。前者告诉我们一个特定的所有者持有多少代币,后者可以用来获得一个所有者拥有的所有 ID。不过也带来了相应的 gas 成本, 可以阅读:调整 NFT 智能合约,减少 70%的铸币 Gas 成本[13]

function tokensOfOwner(address _owner)

external

view

returns (uint[] memory) {

uint tokenCount = balanceOf(_owner);

uint[] memory tokensId = new uint256[](tokenCount “] memory tokensId = new uint256[“);

for (uint i = 0; i < tokenCount; i++) {

tokensId[i] = tokenOfOwnerByIndex(_owner, i);

}

return tokensId;

}

提取合约余额功能

如果我们不能提取发送到合约中的以太币,那么我们所做的所有努力都将付诸东流。

让我们写一个函数,允许我们提取合约的全部余额。这显然需要被标记为onlyOwner。

function withdraw() public payable onlyOwner {

uint balance = address(this).balance;

require(balance > 0, “No ether left to withdraw”);

(bool success, ) = (msg.sender).call{value: balance}(“”);

require(success, “Transfer failed.”);

}

最终合约

我们已经完成了智能合约,代码如下:

//SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import “@openzeppelin/contracts/utils/Counters.sol”;

import “@openzeppelin/contracts/access/Ownable.sol”;

import “@openzeppelin/contracts/utils/math/SafeMath.sol”;

import “@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol”;

contract NFTCollectible is ERC721Enumerable, Ownable {

using SafeMath for uint256;

using Counters for Counters.Counter;

Counters.Counter private _tokenIds;

uint public constant MAX_SUPPLY = 100;

uint public constant PRICE = 0.01 ether;

uint public constant MAX_PER_MINT = 5;

string public baseTokenURI;

constructor(string memory baseURI) ERC721(“NFT Collectible”, “NFTC”) {

setBaseURI(baseURI);

}

function reserveNFTs() public onlyOwner {

uint totalMinted = _tokenIds.current();

require(totalMinted.add(10) < MAX_SUPPLY, “Not enough NFTs left to reserve”);

for (uint i = 0; i < 10; i++) {

_mintSingleNFT();

}

}

function _baseURI() internal view virtual override returns (string memory) {

return baseTokenURI;

}

function setBaseURI(string memory _baseTokenURI) public onlyOwner {

baseTokenURI = _baseTokenURI;

}

function mintNFTs(uint _count) public payable {

uint totalMinted = _tokenIds.current();

require(totalMinted.add(_count) <= MAX_SUPPLY, “Not enough NFTs left!”);

require(_count >0 && _count <= MAX_PER_MINT, “Cannot mint specified number of NFTs.”);

require(msg.value >= PRICE.mul(_count), “Not enough ether to purchase NFTs.”);

for (uint i = 0; i < _count; i++) {

_mintSingleNFT();

}

}

function _mintSingleNFT() private {

uint newTokenID = _tokenIds.current();

_safeMint(msg.sender, newTokenID);

_tokenIds.increment();

}

function tokensOfOwner(address _owner) external view returns (uint[] memory) {

uint tokenCount = balanceOf(_owner);

uint[] memory tokensId = new uint256[](tokenCount “] memory tokensId = new uint256[“);

for (uint i = 0; i < tokenCount; i++) {

tokensId[i] = tokenOfOwnerByIndex(_owner, i);

}

return tokensId;

}

function withdraw() public payable onlyOwner {

uint balance = address(this).balance;

require(balance > 0, “No ether left to withdraw”);

(bool success, ) = (msg.sender).call{value: balance}(“”);

require(success, “Transfer failed.”);

}

}

在本地部署合约

现在让我们做准备在本地环境中模拟,以便之后将我们的合约部署到 Rinkeby 测试网络(或其他的主网)。

在scripts文件夹中,创建一个名为run.js的新文件并添加以下代码:

const { utils } = require(“ethers”);

async function main() {

const baseTokenURI = “ipfs://QmZbWNKJPAjxXuNFSEaksCJVd1M6DaKQViJBYPK2BdpDEP/”;

// Get owner/deployer’s wallet address

const [owner] = await hre.ethers.getSigners();

// Get contract that we want to deploy

const contractFactory = await hre.ethers.getContractFactory(“NFTCollectible”);

// Deploy contract with the correct constructor arguments

const contract = await contractFactory.deploy(baseTokenURI);

// Wait for this transaction to be mined

await contract.deployed();

// Get contract address

console.log(“Contract deployed to:”, contract.address);

// Reserve NFTs

let txn = await contract.reserveNFTs();

await txn.wait();

console.log(“10 NFTs have been reserved”);

// Mint 3 NFTs by sending 0.03 ether

txn = await contract.mintNFTs(3, { value: utils.parseEther(‘0.03’) });

await txn.wait()

// Get all token IDs of the owner

let tokens = await contract.tokensOfOwner(owner.address)

console.log(“Owner has tokens: “, tokens);

}

main()

.then(() => process.exit(0))

.catch((error) => {

console.error(error);

process.exit(1);

});

这是一些 Javascript 代码,利用ethers.js库来部署合约,然后在合约被部署后调用合约的功能。

下面是发生的一系列事情:

  1. 得到部署者/所有者(我们)的地址
  2. 得到我们想要部署的合约。
  3. 发送一个请求,请求部署该合约,并等待矿工处理这个请求并将其添加到区块链上。
  4. 一旦交易被挖出,我们就会得到合约的地址。
  5. 然后调用合约的函数。我们保留了 10 个 NFT,以及通过向合约发送 0.03ETH 来铸造 3 个 NFT,并检查我们拥有的 NFT。请注意,前两个调用需要 Gas(因为它们是写到区块链上的),而第三个只是从区块链上读取。

让我们在本地运行一下:

npx hardhat run scripts/run.js

如果一切顺利,你应该看到类似这样的输出:

智能合约

将合约部署到 Rinkeby 上

为了将我们的合约部署到 Rinkeby,我们需要进行一些设置。

首先,我们需要一个 RPC URL,使我们能够广播合约创建交易。我们将使用 Alchemy 来做这件事。在这里创建一个 Alchemy 账户[14],然后继续创建一个免费的应用程序。

智能合约

确保网络被设置为Rinkeby。

在创建了应用后,进入你的Alchemy 仪表板[15]并选择你的应用程序。这将打开一个新的窗口,在右上方有一个查看密钥的按钮。点击该按钮并选择 HTTP URL。

从这里水龙头[16]获得一些假的 Rinkeby ETH。对于我们的使用情况,0.5 个 ETH 应该是绰绰有余。一旦你获得了这些 ETH,打开你的 Metamask 扩展,并获得有假 ETH 的钱包的私钥(你可以通过账户详情来获取)。

注意:不要公开分享你的 URL 和私钥。

我们将使用dotenv库将上述变量存储为环境变量,并且不会将它们提交到代码库。

创建一个名为.env的新文件,并以下列格式存储你的 URL 和私钥:

API_URL = “<–YOUR ALCHEMY URL HERE–>”

PRIVATE_KEY = “<–YOUR PRIVATE KEY HERE–>”

现在,用以下内容替换你的hardhat.config.js文件:

require(“@nomiclabs/hardhat-waffle”);

require(‘dotenv’).config();

const { API_URL, PRIVATE_KEY } = process.env;

// This is a sample Hardhat task. To learn how to create your own go to

// https://hardhat.org/guides/create-task.html

task(“accounts”, “Prints the list of accounts”, async (taskArgs, hre) => {

const accounts = await hre.ethers.getSigners();

for (const account of accounts) {

console.log(account.address);

}

});

// You need to export an object to set up your config

// Go to https://hardhat.org/config/ to learn more

/**

* @type import(‘hardhat/config’).HardhatUserConfig

*/

module.exports = {

solidity: “0.8.4”,

defaultNetwork: “rinkeby”,

networks: {

rinkeby: {

url: API_URL,

accounts: [PRIVATE_KEY]

}

},

};

我们就快成功了! 运行以下命令:

npx hardhat run scripts/run.js –network rinkeby

脚本的输出与之前得到的非常相似,只是现在已经被部署到真正的区块链上。

记下合约地址:这里是 0x355638a4eCcb7794257f22f50c289d4189F245。

你可以在 Etherscan 上查看这个合约。进入 Etherscan,输入合约地址,应该看到类似这样的内容:

智能合约

在 OpenSea 上查看我们的 NFT

我们的 NFT 现在已经可以在 OpenSea 上使用,不需要我们明确上传。进入testnets.opensea.io[17]并搜索你的合约地址。

这就是我们的藏品的模样:

智能合约

在 Etherscan 上验证合约代码

在 etherscan 上验证我们的合约。这将允许用户看到你的合约的代码,并确保没有任何“有趣的事情”发生。更重要的是,验证代码将允许你的用户将他们的 Metamask 钱包连接到 etherscan,并在 etherscan 上铸造你的 NFT!

在这样做之前,我们需要一个 Etherscan 的 API 密钥。在这里[18]注册一个免费账户,并访问你的 API 密钥。

让我们把这个 API 密钥添加到.env文件中:

ETHERSCAN_API = “<–YOUR ETHERSCAN API KEY–>”

Hardhat 使我们在 Etherscan 上验证合约变得非常简单。让我们安装以下软件包:

npm install @nomiclabs/hardhat-etherscan

接下来,对 hardhat.config.js 进行调整,使其看起来像这样:

require(“@nomiclabs/hardhat-waffle”);

require(“@nomiclabs/hardhat-etherscan”);

require(‘dotenv’).config();

const { API_URL, PRIVATE_KEY, ETHERSCAN_API } = process.env;

// This is a sample Hardhat task. To learn how to create your own go to

// https://hardhat.org/guides/create-task.html

task(“accounts”, “Prints the list of accounts”, async (taskArgs, hre) => {

const accounts = await hre.ethers.getSigners();

for (const account of accounts) {

console.log(account.address);

}

});

// You need to export an object to set up your config

// Go to https://hardhat.org/config/ to learn more

/**

* @type import(‘hardhat/config’).HardhatUserConfig

*/

module.exports = {

solidity: “0.8.4”,

defaultNetwork: “rinkeby”,

networks: {

rinkeby: {

url: API_URL,

accounts: [PRIVATE_KEY]

}

},

etherscan: {

apiKey: ETHERSCAN_API

}

};

现在,运行以下两个命令:

npx hardhat clean

npx hardhat verify –network rinkeby DEPLOYED_CONTRACT_ADDRESS “BASE_TOKEN_URI”

在我们的例子中,第二条命令看起来像这样:

npx hardhat verify –network rinkeby 0x355638a4eCcb777794257f22f50c289d4189F245 “ipfs://QmZbWNKJPAjxXuNFSEaksCJVd1M6DaKQViJBYPK2BdpDEP/”

智能合约

现在,如果访问你的合约的 Rinkeby Etherscan 页面,你应该在合约标签旁边看到一个小的绿色勾。更重要的是,用户现在可以使用 Metamask 连接到 web3,并从 Etherscan 上调用你的合约的功能:

智能合约

自己试试吧。

连接你用来部署合约的账户,从 etherscan 调用withdraw功能。你应该可以将合约中的 0.03 个 ETH 转账到你的钱包里。另外,邀请你的一个朋友连接他们的钱包,通过调用mintNFT函数来铸造一些 NFT。

总结

我们现在有一个已部署的智能合约,可以让用户从我们的合约中铸造 NFT。一个明显的下一步是建立一个 web3 应用程序,让我们的用户可以直接从我们的网站上铸造 NFT。这将是另一个教程[19]的主题。

如果你已经走到了这一步,恭喜你!

最终代码库:https://github.com/rounakbanik/nft-collectible-contract

参考资料

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

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

[3]Tiny 熊: https://learnblockchain.cn/people/15

[4]生成艺术库: https://github.com/rounakbanik/generative-art-nft

[5]头像集合: https://dev.to/rounakbanik/create-generative-nft-art-with-rarities-1n6f

[6]将元数据JSON和媒体文件上传至IPFS: https://dev.to/rounakbanik/working-with-nft-metadata-ipfs-and-pinata-3ieh

[7]YouTube教程: https://www.youtube.com/watch?v=NCwa_xi0Uuc

[8]CryptoZombies: https://cryptozombies.io/en/course/

[9]Buildpace: https://buildspace.so/

[10]这里: https://ipfs.io/ipfs/QmUygfragP8UmCa7aq19AHLttxiLw1ELnqcsQQpM5crgTF

[11]这里: https://ipfs.io/ipfs/QmZbWNKJPAjxXuNFSEaksCJVd1M6DaKQViJBYPK2BdpDEP

[12]Solidity: https://learnblockchain.cn/docs/solidity

[13]调整NFT智能合约,减少70%的铸币Gas成本: https://learnblockchain.cn/article/4388

[14]在这里创建一个Alchemy账户: https://alchemy.com/?r=7d60e34c-b30a-4ffa-89d4-3c4efea4e14b

[15]Alchemy仪表板: https://dashboard.alchemyapi.io/

[16]这里水龙头: https://faucet.rinkeby.io/

[17]testnets.opensea.io: https://testnets.opensea.io/

[18]这里: https://etherscan.io/apis

[19]另一个教程: https://learnblockchain.cn/article/4530

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

责任编辑:Kate

转载声明:本文 由CoinON抓取收录,观点仅代表作者本人,不代表CoinON资讯立场,CoinON不对所包含内容的准确性、可靠性或完整性提供任何明示或暗示的保证。若以此作为投资依据,请自行承担全部责任。

声明:图文来源于网络,如有侵权请联系删除

风险提示:投资有风险,入市需谨慎。本资讯不作为投资理财建议。

(0)
打赏 微信扫一扫 微信扫一扫
上一篇 2022年8月14日 下午2:46
下一篇 2022年8月14日 下午7:22

相关推荐

如何编写 NFT 智能合约

星期日 2022-08-14 19:22:50

简介

在之前的教程中,我们向你展示了如何使用我们的生成艺术库[4]来创建一个头像集合[5],生成符合要求的 NFT 元数据,并将元数据 JSON 和媒体文件上传至 IPFS[6]。

然而,我们还没有把头像铸成 NFT。因此,在本教程中,我们将编写一个智能合约,允许任何人通过支付 Gas 从我们的藏品中铸造一个 NFT。

前提

智能合约

  1. 了解 Javascript 的中级知识(如果你需要复习,我建议使用这个YouTube 教程[7])。
  2. 了解 Solidity 和 OpenZeppelin 合约的中级知识(推荐CryptoZombies[8]和Buildpace[9])。
  3. 在本地电脑上安装 node 和 npm
  4. 准备好一组媒体文件和 NFT 元数据 JSON 上传至 IPFS。(如果你没有这个,我们已经创建了一个玩具集供你实验。你可以在这里[10]找到媒体文件和在这里[11]找到 JSON 元数据文件)。

虽然不满足先决条件的读者可能会跟着做,甚至可以部署一个智能合约,但如果你对你的项目很认真,我们强烈建议找一个知道自己在做什么的开发者。智能合约的开发和部署可能是非常昂贵的,而且在安全缺陷和 bug 方面也不宽容。

设置本地开发环境

智能合约

我们将使用 Hardhat,一个行业标准的以太坊开发环境,来开发、部署和验证我们的智能合约。为项目创建一个空文件夹,并通过在终端运行以下命令初始化一个空 package.json 文件:

mkdir nft-collectible && cd nft-collectible && npm init -y

你现在应该在nft-collectible文件夹内,并有一个名为package.json的文件。

接下来,让我们安装 Hardhat。运行以下命令:

npm install –save-dev hardhat

现在我们可以通过运行以下命令并选择 “Create a basic sample project(创建一个基本样本项目)”来创建项目:

npx hardhat

同意所有的默认值(项目根目录,添加.gitignore,并安装所有样本项目的依赖项)。

让我们检查样本项目是否已经正确安装,运行以下命令:

npx hardhat run scripts/sample-script.js

如果一切顺利,你应该看到像这样的输出:

智能合约

我们现在已经成功地配置了 Hardhat 开发环境。现在安装 OpenZeppelin 合约包。这将使我们能够访问 ERC721 合约(NFT 的标准),以及一些我们以后会遇到的辅助库:

npm install @openzeppelin/contracts

如果我们要公开分享项目的代码(在 GitHub 这样的网站上),我们不想分享敏感信息,比如私钥、Etherscan API 密钥或我们的 Alchemy URL(如果其中一些词对你还没有意义,请不要担心)。因此,让我们安装另一个名为 dotenv 的库:

npm install dotenv

我们现在可以开始开发智能合约了。

编写智能合约

智能合约

在这一节中,我们将在Solidity[12]中编写一个智能合约,允许任何人通过支付所需数量的以太币+Gas 来铸造一定数量的 NFT。

在你项目的contracts文件夹中,创建一个名为NFTCollectible.sol的新文件。

我们将使用 Solidity v8.0,合约将继承 OpenZeppelin 的ERC721Enumerable和Ownable合约。前者有一个 ERC721(NFT)标准的默认实现,此外还有一些在处理 NFT 时有用的辅助函数。后者允许我们在合约的增加管理权限。

除了上述内容,还将使用 OpenZeppelin 的SafeMath和Counters库来分别安全地处理无符号整数运算(通过防止溢出)和 tokenID。

这就是我们合约的骨架,看起来像这样:

//SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import “@openzeppelin/contracts/utils/Counters.sol”;

import “@openzeppelin/contracts/access/Ownable.sol”;

import “@openzeppelin/contracts/utils/math/SafeMath.sol”;

import “@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol”;

contract NFTCollectible is ERC721Enumerable, Ownable {

using SafeMath for uint256;

using Counters for Counters.Counter;

Counters.Counter private _tokenIds;

}

常量和变量

我们的合约需要跟踪某些变量和常量。在本教程中,定义以下内容:

  1. 供应量(Supply):可以铸造的 NFT 的最大数量。
  2. 价格:购买 1 个 NFT 所需的以太币数量。
  3. 每次交易的最大铸币数量:你一次可以铸造的 NFT 的上限。
  4. 代币 URI 前缀(baseTokenURI):包含 JSON 元数据的文件夹的 IPFS URL。

在本教程中,我们将把 1-3 设置为常数。换句话说,一旦合约被部署,我们将无法修改它们。我们将为baseTokenURI编写一个 setter 函数,允许合约的所有者(或部署者)在需要时修改它。

在_tokenIds声明下,添加以下内容:

uint public constant MAX_SUPPLY = 100;

uint public constant PRICE = 0.01 ether;

uint public constant MAX_PER_MINT = 5;

string public baseTokenURI;

注意,常数使用了大写字母。请根据你的项目自由改变常数的值。

构造函数

我们将在构造函数的调用中设置baseTokenURI。还将调用父级构造函数并为 NFT 合约设置名称和符号。

因此,构造函数看起来像这样:

constructor(string memory baseURI) ERC721(“NFT Collectible”, “NFTC”) {

setBaseURI(baseURI);

}

保留 NFT 的功能

作为项目的创建者,你可能想为你自己、团队以及像赠品这样的活动保留一些 NFT 的集合。

让我们写一个函数,允许我们免费铸造一定数量的 NFT(在这里为 10 个)。由于调用这个函数的人只需要支付 Gas 费,显然需要把它标记为onlyOwner,这样只有合约的所有者才能调用它:

function reserveNFTs() public onlyOwner {

uint totalMinted = _tokenIds.current();

require(

totalMinted.add(10) < MAX_SUPPLY, “Not enough NFTs”

);

for (uint i = 0; i < 10; i++) {

_mintSingleNFT();

}

}

我们通过调用tokenIds.current()来检查到目前为止铸造的 NFT 的总数。然后检查是否有足够的 NFT 供我们保留。如果是,我们继续通过调用_mintSingleNFT10 次来铸造 10 个 NFT。

在_mintSingleNFT函数中,真正的魔法发生了。我们稍后将研究它。

设置 baseTokenURI

NFT JSON 元数据可以在这个 IPFS URL 上找到:ipfs://QmZbWNKJPAjxXuNFSEaksCJVd1M6DaKQViJBYPK2BdpDEP/。

当我们把这个设置为baseTokenURI时,OpenZeppelin 的实现会自动推导出每个 token 的 URI。它假定 token1 的元数据在ipfs://QmZbWNKJPAjxXuNFSEaksCJVd1M6DaKQViJBYPK2BdpDEP/1,代币 2 的元数据在ipfs://QmZbWNKJPAjxXuNFSEaksCJVd1M6DaKQViJBYPK2BdpDEP/2,等等

(请注意,这些文件没有.json扩展名)。

相应的函数是:

function _baseURI() internal

view

virtual

override

returns (string memory) {

return baseTokenURI;

}

function setBaseURI(string memory _baseTokenURI) public onlyOwner {

baseTokenURI = _baseTokenURI;

}

在合约部署之后,合约的所有者允许改变baseTokenURI。

Mint NFT 函数

现在让我们把注意力转向主要的 Mint NFT 函数。当用户和客户想从我们的收藏中购买和铸造 NFT 时,他们会调用这个函数。

由于他们要向这个函数发送以太币,我们必须将其标记为 payable.

在真实铸币发生之前,我们需要做三个检查:

  1. 有足够的 NFT 数量供调用者铸造。
  2. 请求的铸币数量超过 0,但少于每笔交易允许的最大 NFT 数量。
  3. 调用者已经发送了足够的以太币来铸造所要求的 NFT 数量。

function mintNFTs(uint _count) public payable {

uint totalMinted = _tokenIds.current();

require(

totalMinted.add(_count) <= MAX_SUPPLY, “Not enough NFTs!”

);

require(

_count > 0 && _count <= MAX_PER_MINT,

“Cannot mint specified number of NFTs.”

);

require(

msg.value >= PRICE.mul(_count),

“Not enough ether to purchase NFTs.”

);

for (uint i = 0; i < _count; i++) {

_mintSingleNFT();

}

}

铸造单个 NFT 函数

最后让我们看看私有的_mintSingleNFT()函数,每当我们(或第三方)想铸造一个 NFT 时,都会调用这个函数:

function _mintSingleNFT() private {

uint newTokenID = _tokenIds.current();

_safeMint(msg.sender, newTokenID);

_tokenIds.increment();

}

这里发生的事情:

  1. 得到当前还没有被铸造的 ID。
  2. 使用 OpenZeppelin 已经定义的_safeMint()函数,将 NFT ID 分配给调用该函数的账户。
  3. 我们将 tokenID 的计数器递增 1。

在发生任何铸币行为之前,代币 ID 为 0。

当这个函数第一次被调用时,newTokenID是 0。调用safeMint()将 ID 为 0 的 NFT 分配给调用合约函数的人,然后计数器被递增到 1。

下次调用此函数时,_newTokenID的值为 1。调用safeMint()将 ID 为 1 的 NFT 分配给……我想你能明白这个要点。

注意,我们不需要为每个 NFT 再次设置元数据。设置baseTokenURI可以确保每个 NFT 自动获得正确的元数据(存储在 IPFS 中)。

获取一个特定账户所拥有的所有代币

如果你打算给你的 NFT 持有人提供类似列表类的功能,你会想每个用户持有哪些 NFT。

让我们写一个简单的函数,返回一个特定持有人拥有的所有 ID。

ERC721Enumerable 的 “balanceOf “和 “tokenOfOwnerByIndex “函数使之变得超级简单。前者告诉我们一个特定的所有者持有多少代币,后者可以用来获得一个所有者拥有的所有 ID。不过也带来了相应的 gas 成本, 可以阅读:调整 NFT 智能合约,减少 70%的铸币 Gas 成本[13]

function tokensOfOwner(address _owner)

external

view

returns (uint[] memory) {

uint tokenCount = balanceOf(_owner);

uint[] memory tokensId = new uint256[](tokenCount “] memory tokensId = new uint256[“);

for (uint i = 0; i < tokenCount; i++) {

tokensId[i] = tokenOfOwnerByIndex(_owner, i);

}

return tokensId;

}

提取合约余额功能

如果我们不能提取发送到合约中的以太币,那么我们所做的所有努力都将付诸东流。

让我们写一个函数,允许我们提取合约的全部余额。这显然需要被标记为onlyOwner。

function withdraw() public payable onlyOwner {

uint balance = address(this).balance;

require(balance > 0, “No ether left to withdraw”);

(bool success, ) = (msg.sender).call{value: balance}(“”);

require(success, “Transfer failed.”);

}

最终合约

我们已经完成了智能合约,代码如下:

//SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import “@openzeppelin/contracts/utils/Counters.sol”;

import “@openzeppelin/contracts/access/Ownable.sol”;

import “@openzeppelin/contracts/utils/math/SafeMath.sol”;

import “@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol”;

contract NFTCollectible is ERC721Enumerable, Ownable {

using SafeMath for uint256;

using Counters for Counters.Counter;

Counters.Counter private _tokenIds;

uint public constant MAX_SUPPLY = 100;

uint public constant PRICE = 0.01 ether;

uint public constant MAX_PER_MINT = 5;

string public baseTokenURI;

constructor(string memory baseURI) ERC721(“NFT Collectible”, “NFTC”) {

setBaseURI(baseURI);

}

function reserveNFTs() public onlyOwner {

uint totalMinted = _tokenIds.current();

require(totalMinted.add(10) < MAX_SUPPLY, “Not enough NFTs left to reserve”);

for (uint i = 0; i < 10; i++) {

_mintSingleNFT();

}

}

function _baseURI() internal view virtual override returns (string memory) {

return baseTokenURI;

}

function setBaseURI(string memory _baseTokenURI) public onlyOwner {

baseTokenURI = _baseTokenURI;

}

function mintNFTs(uint _count) public payable {

uint totalMinted = _tokenIds.current();

require(totalMinted.add(_count) <= MAX_SUPPLY, “Not enough NFTs left!”);

require(_count >0 && _count <= MAX_PER_MINT, “Cannot mint specified number of NFTs.”);

require(msg.value >= PRICE.mul(_count), “Not enough ether to purchase NFTs.”);

for (uint i = 0; i < _count; i++) {

_mintSingleNFT();

}

}

function _mintSingleNFT() private {

uint newTokenID = _tokenIds.current();

_safeMint(msg.sender, newTokenID);

_tokenIds.increment();

}

function tokensOfOwner(address _owner) external view returns (uint[] memory) {

uint tokenCount = balanceOf(_owner);

uint[] memory tokensId = new uint256[](tokenCount “] memory tokensId = new uint256[“);

for (uint i = 0; i < tokenCount; i++) {

tokensId[i] = tokenOfOwnerByIndex(_owner, i);

}

return tokensId;

}

function withdraw() public payable onlyOwner {

uint balance = address(this).balance;

require(balance > 0, “No ether left to withdraw”);

(bool success, ) = (msg.sender).call{value: balance}(“”);

require(success, “Transfer failed.”);

}

}

在本地部署合约

现在让我们做准备在本地环境中模拟,以便之后将我们的合约部署到 Rinkeby 测试网络(或其他的主网)。

在scripts文件夹中,创建一个名为run.js的新文件并添加以下代码:

const { utils } = require(“ethers”);

async function main() {

const baseTokenURI = “ipfs://QmZbWNKJPAjxXuNFSEaksCJVd1M6DaKQViJBYPK2BdpDEP/”;

// Get owner/deployer’s wallet address

const [owner] = await hre.ethers.getSigners();

// Get contract that we want to deploy

const contractFactory = await hre.ethers.getContractFactory(“NFTCollectible”);

// Deploy contract with the correct constructor arguments

const contract = await contractFactory.deploy(baseTokenURI);

// Wait for this transaction to be mined

await contract.deployed();

// Get contract address

console.log(“Contract deployed to:”, contract.address);

// Reserve NFTs

let txn = await contract.reserveNFTs();

await txn.wait();

console.log(“10 NFTs have been reserved”);

// Mint 3 NFTs by sending 0.03 ether

txn = await contract.mintNFTs(3, { value: utils.parseEther(‘0.03’) });

await txn.wait()

// Get all token IDs of the owner

let tokens = await contract.tokensOfOwner(owner.address)

console.log(“Owner has tokens: “, tokens);

}

main()

.then(() => process.exit(0))

.catch((error) => {

console.error(error);

process.exit(1);

});

这是一些 Javascript 代码,利用ethers.js库来部署合约,然后在合约被部署后调用合约的功能。

下面是发生的一系列事情:

  1. 得到部署者/所有者(我们)的地址
  2. 得到我们想要部署的合约。
  3. 发送一个请求,请求部署该合约,并等待矿工处理这个请求并将其添加到区块链上。
  4. 一旦交易被挖出,我们就会得到合约的地址。
  5. 然后调用合约的函数。我们保留了 10 个 NFT,以及通过向合约发送 0.03ETH 来铸造 3 个 NFT,并检查我们拥有的 NFT。请注意,前两个调用需要 Gas(因为它们是写到区块链上的),而第三个只是从区块链上读取。

让我们在本地运行一下:

npx hardhat run scripts/run.js

如果一切顺利,你应该看到类似这样的输出:

智能合约

将合约部署到 Rinkeby 上

为了将我们的合约部署到 Rinkeby,我们需要进行一些设置。

首先,我们需要一个 RPC URL,使我们能够广播合约创建交易。我们将使用 Alchemy 来做这件事。在这里创建一个 Alchemy 账户[14],然后继续创建一个免费的应用程序。

智能合约

确保网络被设置为Rinkeby。

在创建了应用后,进入你的Alchemy 仪表板[15]并选择你的应用程序。这将打开一个新的窗口,在右上方有一个查看密钥的按钮。点击该按钮并选择 HTTP URL。

从这里水龙头[16]获得一些假的 Rinkeby ETH。对于我们的使用情况,0.5 个 ETH 应该是绰绰有余。一旦你获得了这些 ETH,打开你的 Metamask 扩展,并获得有假 ETH 的钱包的私钥(你可以通过账户详情来获取)。

注意:不要公开分享你的 URL 和私钥。

我们将使用dotenv库将上述变量存储为环境变量,并且不会将它们提交到代码库。

创建一个名为.env的新文件,并以下列格式存储你的 URL 和私钥:

API_URL = “<–YOUR ALCHEMY URL HERE–>”

PRIVATE_KEY = “<–YOUR PRIVATE KEY HERE–>”

现在,用以下内容替换你的hardhat.config.js文件:

require(“@nomiclabs/hardhat-waffle”);

require(‘dotenv’).config();

const { API_URL, PRIVATE_KEY } = process.env;

// This is a sample Hardhat task. To learn how to create your own go to

// https://hardhat.org/guides/create-task.html

task(“accounts”, “Prints the list of accounts”, async (taskArgs, hre) => {

const accounts = await hre.ethers.getSigners();

for (const account of accounts) {

console.log(account.address);

}

});

// You need to export an object to set up your config

// Go to https://hardhat.org/config/ to learn more

/**

* @type import(‘hardhat/config’).HardhatUserConfig

*/

module.exports = {

solidity: “0.8.4”,

defaultNetwork: “rinkeby”,

networks: {

rinkeby: {

url: API_URL,

accounts: [PRIVATE_KEY]

}

},

};

我们就快成功了! 运行以下命令:

npx hardhat run scripts/run.js –network rinkeby

脚本的输出与之前得到的非常相似,只是现在已经被部署到真正的区块链上。

记下合约地址:这里是 0x355638a4eCcb7794257f22f50c289d4189F245。

你可以在 Etherscan 上查看这个合约。进入 Etherscan,输入合约地址,应该看到类似这样的内容:

智能合约

在 OpenSea 上查看我们的 NFT

我们的 NFT 现在已经可以在 OpenSea 上使用,不需要我们明确上传。进入testnets.opensea.io[17]并搜索你的合约地址。

这就是我们的藏品的模样:

智能合约

在 Etherscan 上验证合约代码

在 etherscan 上验证我们的合约。这将允许用户看到你的合约的代码,并确保没有任何“有趣的事情”发生。更重要的是,验证代码将允许你的用户将他们的 Metamask 钱包连接到 etherscan,并在 etherscan 上铸造你的 NFT!

在这样做之前,我们需要一个 Etherscan 的 API 密钥。在这里[18]注册一个免费账户,并访问你的 API 密钥。

让我们把这个 API 密钥添加到.env文件中:

ETHERSCAN_API = “<–YOUR ETHERSCAN API KEY–>”

Hardhat 使我们在 Etherscan 上验证合约变得非常简单。让我们安装以下软件包:

npm install @nomiclabs/hardhat-etherscan

接下来,对 hardhat.config.js 进行调整,使其看起来像这样:

require(“@nomiclabs/hardhat-waffle”);

require(“@nomiclabs/hardhat-etherscan”);

require(‘dotenv’).config();

const { API_URL, PRIVATE_KEY, ETHERSCAN_API } = process.env;

// This is a sample Hardhat task. To learn how to create your own go to

// https://hardhat.org/guides/create-task.html

task(“accounts”, “Prints the list of accounts”, async (taskArgs, hre) => {

const accounts = await hre.ethers.getSigners();

for (const account of accounts) {

console.log(account.address);

}

});

// You need to export an object to set up your config

// Go to https://hardhat.org/config/ to learn more

/**

* @type import(‘hardhat/config’).HardhatUserConfig

*/

module.exports = {

solidity: “0.8.4”,

defaultNetwork: “rinkeby”,

networks: {

rinkeby: {

url: API_URL,

accounts: [PRIVATE_KEY]

}

},

etherscan: {

apiKey: ETHERSCAN_API

}

};

现在,运行以下两个命令:

npx hardhat clean

npx hardhat verify –network rinkeby DEPLOYED_CONTRACT_ADDRESS “BASE_TOKEN_URI”

在我们的例子中,第二条命令看起来像这样:

npx hardhat verify –network rinkeby 0x355638a4eCcb777794257f22f50c289d4189F245 “ipfs://QmZbWNKJPAjxXuNFSEaksCJVd1M6DaKQViJBYPK2BdpDEP/”

智能合约

现在,如果访问你的合约的 Rinkeby Etherscan 页面,你应该在合约标签旁边看到一个小的绿色勾。更重要的是,用户现在可以使用 Metamask 连接到 web3,并从 Etherscan 上调用你的合约的功能:

智能合约

自己试试吧。

连接你用来部署合约的账户,从 etherscan 调用withdraw功能。你应该可以将合约中的 0.03 个 ETH 转账到你的钱包里。另外,邀请你的一个朋友连接他们的钱包,通过调用mintNFT函数来铸造一些 NFT。

总结

我们现在有一个已部署的智能合约,可以让用户从我们的合约中铸造 NFT。一个明显的下一步是建立一个 web3 应用程序,让我们的用户可以直接从我们的网站上铸造 NFT。这将是另一个教程[19]的主题。

如果你已经走到了这一步,恭喜你!

最终代码库:https://github.com/rounakbanik/nft-collectible-contract

参考资料

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

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

[3]Tiny 熊: https://learnblockchain.cn/people/15

[4]生成艺术库: https://github.com/rounakbanik/generative-art-nft

[5]头像集合: https://dev.to/rounakbanik/create-generative-nft-art-with-rarities-1n6f

[6]将元数据JSON和媒体文件上传至IPFS: https://dev.to/rounakbanik/working-with-nft-metadata-ipfs-and-pinata-3ieh

[7]YouTube教程: https://www.youtube.com/watch?v=NCwa_xi0Uuc

[8]CryptoZombies: https://cryptozombies.io/en/course/

[9]Buildpace: https://buildspace.so/

[10]这里: https://ipfs.io/ipfs/QmUygfragP8UmCa7aq19AHLttxiLw1ELnqcsQQpM5crgTF

[11]这里: https://ipfs.io/ipfs/QmZbWNKJPAjxXuNFSEaksCJVd1M6DaKQViJBYPK2BdpDEP

[12]Solidity: https://learnblockchain.cn/docs/solidity

[13]调整NFT智能合约,减少70%的铸币Gas成本: https://learnblockchain.cn/article/4388

[14]在这里创建一个Alchemy账户: https://alchemy.com/?r=7d60e34c-b30a-4ffa-89d4-3c4efea4e14b

[15]Alchemy仪表板: https://dashboard.alchemyapi.io/

[16]这里水龙头: https://faucet.rinkeby.io/

[17]testnets.opensea.io: https://testnets.opensea.io/

[18]这里: https://etherscan.io/apis

[19]另一个教程: https://learnblockchain.cn/article/4530

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

责任编辑:Kate