如何发行一款NFT(上)

希望通过本文,可以让有发行NFT想法的小伙伴,发行一款自己的NFT。

如何发行一款社区概念的NFT,一般是有4个步骤

  1. 发起人完成白皮书
  2. 完成NFT设计并编写智能合约代码发布到链上
  3. 完成NFT社区官网,一般包含白皮书简介,mint,NFT列表展览等功能
  4. 社区运营以及持续为达成白皮书的目标而合作迭代

我们选择技术点涉及较多的2、3步骤和大家讨论。分为上下两篇文章,本文将重点讲解第2点如何完成NFT智能合约的开发、部署、交互以及在交易市场查看自己发行的NFT。下篇文章会重点讲解第3点NFT社区官网的搭建。

合约开发环境选择

目前主流的智能合约开发环境有3个:remix,hardhat, truffle。remix是浏览器IDE,虽然开箱即用,上手简单,但在灵活性和功能完善度上不如hardhat, truffle。hardhat, truffle相比较,truffle是老牌主流框架,hardhat是新起之秀,但目前也足够稳定。
我们最终选择hardhat作为我们的本地合约开发环境。

初始化工程

//创建一款web3探索者的nft
mkdir  nft-web3-explorer

//进入目录
cd nft-web3-explorer

//初始化项目,根据提示填写即可,packname和description填写即可
npm init

//添加hardhat依赖
npm install --save-dev hardhat


/*使用脚手架搭建项目,我们选择Create a basic sample project
可以帮助我们创建1个demo工程并按照所需的依赖*/
npx hardhat

创建完成后,我们看看我们的目录结构

  • contracts目录用来存放我们的智能合约代码
  • scripts用来存放我们的脚本,比如合约部署就会依赖其中的脚本
  • test目录用来存放我们为智能合约编写的测试代码
  • hardhat.config.js是关于hardhat框架的一些配置,比如solidity版本等
  • package.json是npm的相关配置

使用vscode打开该工程,最好安装vscode的solidity插件。

编写合约代码

目前就可以开始写合约代码了,本文主要是介绍NFT的发行,我们简单来实现一份NFT智能合约代码。

//安装openzeppelin/contracts依赖,内置较多合约协议的实现以及工具代码
npm install @openzeppelin/contracts

我们在contracts目录下创建NFT_WEB3_EXPOLRER.sol的文件。

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";

contract NFT_WEB3_EXPLORER is ERC721, ERC721Enumerable, Ownable {
    string private _baseURIextended;
    //我们设置最多可以mint1000个
    uint256 public constant MAX_SUPPLY = 1000;
    //每个mint的价格是0.01EHT
    uint256 public constant PRICE_PER_TOKEN = 0.01 ether;

    constructor() ERC721("nft_web3_explorer", "NFT_WEB3_EXPLORER") {
    }

    function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal override(ERC721, ERC721Enumerable) {
        super._beforeTokenTransfer(from, to, tokenId);
    }

    function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, ERC721Enumerable) returns (bool) {
        return super.supportsInterface(interfaceId);
    }

    function setBaseURI(string memory baseURI_) external onlyOwner() {
        _baseURIextended = baseURI_;
    }

    function _baseURI() internal view virtual override returns (string memory) {
        return _baseURIextended;
    }

    function mint(uint numberOfTokens) public payable {
        uint256 ts = totalSupply();
        require(ts + numberOfTokens <= MAX_SUPPLY, "Purchase would exceed max tokens");
        require(PRICE_PER_TOKEN * numberOfTokens <= msg.value, "Ether value sent is not correct");

        for (uint256 i = 0; i < numberOfTokens; i++) {
            _safeMint(msg.sender, ts + i);
        }
    }

    function withdraw() public onlyOwner {
        uint balance = address(this).balance;
        payable(msg.sender).transfer(balance);
    }
}

非常简单,这源于ERC721协议的帮助。该代码不具备上线标准,因为没有白名单,每个地址限制mint多少个等,仅供示意。建议发行NFT前需要好好设计合约,

合约编译

//合约编译
npx hardhat compile

合约部署

修改scripts目录下自动生成的脚本改名为deploy.js并修改内部代码

const hre = require("hardhat");

async function main() {
  // Hardhat always runs the compile task when running scripts with its command
  // line interface.
  //
  // If this script is run directly using `node` you may want to call compile
  // manually to make sure everything is compiled
  // await hre.run('compile');

  // We get the contract to deploy
  const Nft_web3_explrer = await hre.ethers.getContractFactory("NFT_WEB3_EXPLORER");
  const nft_web3_explrer = await Nft_web3_explrer.deploy();

  await nft_web3_explrer.deployed();

  console.log("NFT_WEB3_EXPLORER deployed to:", nft_web3_explorer.address);
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

选择节点服务代理

此时,因为我们并不想在本地运行1个以太坊节点,所以我们使用节点服务代理(管理节点),我们与他们进行api交互,他们会在云上管理的节点中执行对应的节点操作。这里我们选用与MetaMask同一服务代理 https://infura.io/
注册账号后,我们创建1个项目

测试网络环境搭建

我们首先需要在测试网络中部署,由于很多NFT交易市场只支持rinkeby,所以我们选择rinkeby作为我们的测试网络。

因为在代码执行过程中会用到公钥、私钥、API等数据,但这些数据又不能编码到代码中,因为如果我们使用git来管理项目,信息将会泄露。我们使用dotenv存放部署合约以及和合约交互需要用到的数据。

//添加dotenv依赖
npm install dotenv

//工程根目录下创建变量存储文件
touch .env

为.env增加内容

PRIVATE_KEY=导出你metamask的私钥填在这里
API=找到节点服务代理URL填在这里
PUBLIC_KEY=metamask地址
NETWORK=rinkeby
API_KEY=节点服务代理的ProjectID
  • metamask私钥导出:打开钱包-账号详情->导出私钥。
  • 服务代理URL:将网络切换到ROPSTEN,复制下面的https链接。

修改hardhat.config.js

require("@nomiclabs/hardhat-waffle");

// This is a sample Hardhat task. To learn how to create your own go to
// https://hardhat.org/guides/create-task.html
require('dotenv').config();
const { API, PRIVATE_KEY } = process.env;

// 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: {
    hardhat: {},
    rinkeby: {
      url: API,
      accounts: [`0x${PRIVATE_KEY}`]
    }
  },
};

获取测试网络ETH
因为合约测试需要消耗ETH,我们需要获取一些测试完了的ETH,我们打开https://fauceth.komputing.org/ 选择rinkeby网络,填入我们的钱包地址即可获取。

此时我们将钱包切换到rinkeby测试网络,可以看到钱包中已经有一些ETH了。

执行合约部署

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

//执行完成后得到提示,代表合约部署完成
NFT_WEB3_EXPOLRER deployed to: {合约地址}

此时我们可以在https://rinkeby.etherscan.io/搜索我们的合约地址找到我们的合约信息

为NFT设置资源

现在的NFT仅仅包含tokenID,我们需要为其设置资源, 在ERC721的实现中我们可以看到tokenURI方法的实现,各个交易市场就是调用该方法来读取NFT资源信息的

    /**
     * @dev See {IERC721Metadata-tokenURI}.
     */
    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");

        string memory baseURI = _baseURI();
        return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : "";
    }

我们可以看到这里是将BaseURI和tokenID拼接来读取tokenURI的,所以我们也按照这种格式来准备资源。

准备资源

//创建资源文件
mkdir res
cd res
//存放图片
mkdir  img
//存放metadata信息
mkdir  metadata

由于本文是示例项目,没有找专门的设计师设计图片,我们网上找了7张图片。将7张图片放入img目录下,依次命名为0.png ~ 7.png

我们将7张图片上传到ipfs中,这里和以太坊网络一样,由于我们不想运行本地ipfs节点,所以我们寻找节点服务提供商进行上传。我们使用https://app.pinata.cloud/选择上传目录进行将img目录上传
上传完成后的图片资源

接下来我们准备metadata文件,在metadata目录下新建7个文件依次命名为0~7,我们看下0号文件信息

{
    "name": "nft-web3-explorer",
    "attributes": [
        {
            "trait_type": "tokenID",
            "value": "0"
        }
    ],
    "description": "nft-web3-explorer image",
    //填入上面刚刚上传完成的图片地址
    "image": "ipfs://QmZ3Y31SwLU77CDfBoL5MphuSmrv414d2ZyunVcbNAJQRQ/0.png"
}

设置BaseURI

我们需要将刚刚上传后的metadata文件地址设置给合约的baseURI,这样各个平台在使用tokenURI获取资源信息才可以获取到。

编写代码与合约交互设置BaseURI,在scripts目录下新建setBaseURI.js文件。

setBaseURI.js

require("dotenv").config()
const hre = require("hardhat");
const PRIVATE_KEY = process.env.PRIVATE_KEY
const NETWORK = process.env.NETWORK
const API_KEY = process.env.API_KEY


const provider = new hre.ethers.providers.InfuraProvider(NETWORK, API_KEY);
//编译完成合约会自动生成
const abi = require("../artifacts/contracts/NFT_WEB3_EXPLORER.sol/NFT_WEB3_EXPLORER.json").abi
const contractAddress = "合约地址"
const contract = new hre.ethers.Contract(contractAddress, abi, provider)
const wallet = new hre.ethers.Wallet(PRIVATE_KEY, provider)
const baseURI = "metadata文件地址"

async function main() {
  const contractWithSigner = contract.connect(wallet);
  //调用setBaseURI方法
  const tx = await contractWithSigner.setBaseURI(baseURI)
  console.log(tx.hash);
  await tx.wait();
  console.log("setBaseURL success");
}

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

在这里我们与合约交互主要是使用ethers,目前主流封装与合约交互的js库有2个web3.js与ethers.js,此处使用ethers的原因是hardhat默认和ethers配合。

//执行setBaseURI脚本
npx hardhat --network rinkeby run scripts/setBaseURI.js

mint测试

因为只有mint过的tokenID才会展示在交易市场中,所以我们需要编写代码进行mint测试(实际场景,该操作应该由前端页面调用完成)。mint.js

require("dotenv").config()
const hre = require("hardhat");
const PRIVATE_KEY = process.env.PRIVATE_KEY
const NETWORK = process.env.NETWORK
const API_KEY = process.env.API_KEY


const provider = new hre.ethers.providers.InfuraProvider(NETWORK, API_KEY);
const abi = require("../artifacts/contracts/NFT_WEB3_EXPOLRER.sol/NFT_WEB3_EXPOLRER.json").abi
const contractAddress = "合约地址"
const contract = new hre.ethers.Contract(contractAddress, abi, provider)
const wallet = new hre.ethers.Wallet(PRIVATE_KEY, provider)

async function main() {
    const contractWithSigner = contract.connect(wallet);
    //获取mint需要多少ETH
    const price = await contract.PRICE_PER_TOKEN();
    console.log("price is" + price);
    //调用mint方法,支付mint费用
    const tx = await contractWithSigner.mint(1, { value: price});
    console.log(tx.hash);
    await tx.wait();
    console.log("mint success");
}

main()
    .then(() => process.exit(0))
    .catch((error) => {
        console.error(error);
        process.exit(1);
    });
//执行mint脚本
npx hardhat --network rinkeby run scripts/mint.js

查看NFT

此时我们去opensea或looksrare等NFT市场查看我们的NFT即可(个人建议looksrare查看,opensea测试网络速度很慢,而且metadata信息更新也比较慢)

结尾

本篇文章我们介绍了如何一步步编写合约,部署以及与合约交互完成将NFT发布到链上,下篇文章将介绍如何搭建NFT的官网。

========

往期文章

Layer2:
真正理解 Layer2
Immutable X白皮书(译)
跨链:
关于Cosmos的研究
基础理论:
关于区块链不可能三角的研究
关于零知识证明的研究
以太坊:
以太坊技术系列-以太坊数据结构
以太坊技术系列-以太坊共识机制
以太坊技术系列-钱包-以太坊中的账户
聊一聊智能合约
去中心化存储:
去中心化存储的那些事(上)
去中心化存储那些事(下)
其他公链:
Flow白皮书1-共识和计算分离(译)
智能合约实战:
智能合约升级详解

Subscribe to explorer_of_web3
Receive the latest updates directly to your inbox.
Verification
This entry has been permanently stored onchain and signed by its creator.