希望通过本文,可以让有发行NFT想法的小伙伴,发行一款自己的NFT。
如何发行一款社区概念的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
创建完成后,我们看看我们的目录结构
使用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
修改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仅仅包含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"
}
我们需要将刚刚上传后的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过的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
此时我们去opensea或looksrare等NFT市场查看我们的NFT即可(个人建议looksrare查看,opensea测试网络速度很慢,而且metadata信息更新也比较慢)
本篇文章我们介绍了如何一步步编写合约,部署以及与合约交互完成将NFT发布到链上,下篇文章将介绍如何搭建NFT的官网。
========
往期文章
Layer2:
真正理解 Layer2
Immutable X白皮书(译)
跨链:
关于Cosmos的研究
基础理论:
关于区块链不可能三角的研究
关于零知识证明的研究
以太坊:
以太坊技术系列-以太坊数据结构
以太坊技术系列-以太坊共识机制
以太坊技术系列-钱包-以太坊中的账户
聊一聊智能合约
去中心化存储:
去中心化存储的那些事(上)
去中心化存储那些事(下)
其他公链:
Flow白皮书1-共识和计算分离(译)
智能合约实战:
智能合约升级详解