本文尝试发行一款NFT
,我们从合约出发而不是直接使用Opensea 来搞定定制化发布,在此之前你需要先熟悉Solidity
这门合约语言,并拥有Metamask
钱包和账户。
NFT基本认识 NFT是什么 NFT是Non-fungible token的缩写,即非同质化代币,是一种在区块链上的数据单位,既然是token,那么其就具备了资产属性。这种资产不可互换,可以被确权。技术上作品本身可以被复制,但是作品的拥有权被区块链所记录且完整追踪,所有权是唯一的。另一方面,非同质化是说:所有的房屋都是不同的,没有两只小猫是一样的。NFT可区分,必须分别跟踪每个所有者。和ERC20的区别就在此:每一个token在ERC20都是一样的,而且可以分割。
ERC721是什么 ERC是Ethereum Request for Comments的缩写,是以太坊智能合约开发人员使用的技术文档,定义一组准则或协议。比如ERC20定义了代币标准,ERC721 定义了NFT标准。其中ERC721依赖ERC165。
如何发行NFT 一种是使用Opensea
这类网站发行,准备好Gas fee
后,只需要点几次鼠标,写一点介绍就可以了。我们这次不准备用这种方式,而是通过智能合约来发布一款NFT。
发行NFT的流程 首先我们要明确我们的NFT所锚定的对象,是一组图片还是一组音乐、N段故事、贷款,亦或是实体财产。我们发行的NFT的数量,发行价格,是否包含free mint,白名单等等一系列经济模型。简单说有如下几步:
熟读ERC721合约的标准,看一看现有的实现
设计NFT,故事,白皮书等
编写NFT合约,并上链;周边配套的开发,比如一款DApp ,DApp的开发不是本篇的重点,我们只关注合约部分
做社区,拉盘
安装与初始化本地Hardhat Ethereum合约开发的工作流的市场份额当前被Hardhat 占据主导地位,本篇我们使用Hardhat
开发我们的合约。请按照下面的步骤来配置:
1 2 3 4 5 6 7 8 9 $ mkdir my-erc721 $ cd my-erc721 $ npm init $ npm install --save-dev hardhat $ npx hardhat ... 👷 Welcome to Hardhat v2.9.6 👷 ✔ What do you want to do ? · Create a basic sample project ...
安装好后,我们会在contracts
目录内编写我们的智能合约,在scripts
中编写js
语言的部署脚本,并在hardhat.config.js
配置部署和编译信息等。
NFT合约编写 目前最为流行的ERC721
标准实现当然是鼎鼎大名的OpenZeppelin
,他的实现在安全性、完成度、可维护性和测试等方面都做的很不错。但是其gas fee
存在一些较大消耗。这里我们使用知名NFT项目azuki 的ERC721A 实现。这是一版改进实现,特别重视了合约的运行效率,优化了gas fee
,而且合约依赖更少。
先安装合约:
1 2 $ npm install --save-dev erc721a $ npm install @openzeppelin/contracts
接着我们编写合约代码:
1 2 3 4 5 6 7 8 9 10 11 12 pragma solidity ^0.8 .4 ; import "erc721a/contracts/ERC721A.sol" ; contract ZY is ERC721A { constructor ( ) ERC721A ("Zy" , "ZY" ) {} function mint (uint256 quantity ) external payable { _safeMint(msg.sender, quantity); } }
因为Azuki的ERC721A已经帮助我们实现绝大部分功能了,所以最简单的使用就是定义mint
函数。当然因为没有定义白名单,mint相关的限制等(而这些需要根据白皮书的内容来确定),这个合约并非产品级代码,还需要大量的细化特别是安全性相关的测试。
完成后编译合约:
改写默认的sample-scripts.js
部署脚本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const hre = require ("hardhat" );async function main ( ) { const NFT = await hre.ethers.getContractFactory("NFT" ); const nft = await NFT.deploy(); await nft.deployed(); console .log("My NFT deployed to:" , nft.address); } main() .then(() => process.exit(0 )) .catch((error ) => { console .error(error); process.exit(1 ); });
使用Infura进行线上测试 Infura是最早和目前最大的Ethereum的Relay Network解决方案,提供一些公开的Gataway节点。我们会与Infura的api进行交互,而Infura作为中介会在他们的云上管理的节点中执行相关操作完成最终调用。
配置rinkeby 这是NFT交易中主流的测试网络。
1 2 3 4 5 6 7 8 $ npm install dotenv $ touch .env PRIVATE_KEY=[打开钱包-账号详情->导出私钥] 把私钥填写在这里,切忌不要透露私钥给任何人 API=https://rinkeby.infura.io/v3/{以及你的ProjectID,从Infura复制} PUBLIC_KEY=metamask钱包地址 NETWORK=rinkeby API_KEY=Infura上新建项目的ProjectID
配置hardhat.config.js
,以下为新增内容(不含既有内容):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 require ('dotenv' ).config();const { API, PRIVATE_KEY } = process.env;module .exports = { solidity : "0.8.4" , defaultNetwork : "rinkeby" , networks : { hardhat : {}, rinkeby : { url : API, accounts : [`0x${PRIVATE_KEY} ` ] } }, };
从Rinkeby Authenticated Faucet 水龙头获取一些测试用ETH。我们复制Metamask钱包地址,并发一条twitter,然后复制这条推特的地址输入以获得以太:
如果拿不到那就多试几个,总有可以的:Paradigm Rinkeby Faucet 。当我们的钱包中有以太后,开始部署合约,并在rinkeby Etherscan 找到我们的合约信息:
1 2 $ npx hardhat --network rinkeby run scripts/sample-script.js My NFT deployed to: 0x...[合约地址]
部署信息查询如下:
一些具体信息,比如gas fee:
mint第一个NFT 编写我们的mint脚本scripts/mint.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 require ("dotenv" ).config()const hh = require ("hardhat" );const PRIVATE_KEY = process.env.PRIVATE_KEYconst NETWORK = process.env.NETWORKconst API_KEY = process.env.API_KEYconst provider = new hh.ethers.providers.InfuraProvider(NETWORK, API_KEY);const abi = require ("../artifacts/contracts/NFT.sol/ZY.json" ).abiconst contractAddress = "合约地址在这里补充" const contract = new hh.ethers.Contract(contractAddress, abi, provider)const wallet = new hh.ethers.Wallet(PRIVATE_KEY, provider)async function main ( ) { const contractWithSigner = contract.connect(wallet); const price = 100 ; console .log("price is " + price); 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 ); });
接着执行合约,查看效果:
1 2 3 4 $ npx hardhat --network rinkeby run scripts/mint.js price is 100 0x36d2eedc4b...[transaction的Id] mint success
使用去中心文件服务设置资源 我们选择的NFT是没有和任何资源(图片、音乐等)锚定的,这需要我们新手设定,详见IERC721 Metadata-tokenURI
。在ERC721A
中,我们可以找到_baseURI
函数的代码,这个函数的返回值就是资源的根目录,也就是说NFT的资源。默认是返回''
,也就是无资源设定,我们需要override并加入可以设定资源URI的方法:
1 2 3 4 5 6 7 8 function _baseURI ( ) internal view virtual returns (string memory )function tokenURI (uint256 tokenId ) override view public returns (string memory )
我们选择IPFS 服务来存放我们的资源文件(mirror.xyz使用Arweave服务,考虑到支持度的问题,可能得用https网关,所以我们暂时还是选用受众较广的IPFS)。这里可以选用Pinata 。注册账户后按照提示上传我们事先准备好的图片资源文件。这里我们可以选择上传整个文件夹,而不是单张图片。然后我们准备metadata文件夹,并上传:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 $ mkdir img # 放入图片,名字用数字从0开始,比如0.png, 1.png .. ... $ mkdir metadata # 对于每一张图片,都准备一个metadata,文件名是0, 1, .. $ cd metadata $ touch 0 $ vim 0 # 编辑metadata文件 # 文件0中内容如下示例: { "name": "my-nft", "attributes": [ { "trait_type": "tokenID", "value": "0" } ], "description": "a image", # 下面的image填写Pinata上面的CID地址;当然也可以是http的 "image": "ipfs://QmVxF5gdE4hu27tGp3jTfxf7j3cBzhmyYcZeFTP9irgmdQ/0.png" }
这里给出一个带有白名单、free mint、可以self destruct
和可设定外部资源的合约,仅供参考:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 pragma solidity ^0.8 .4 ; import "erc721a/contracts/ERC721A.sol" ;import "@openzeppelin/contracts/access/Ownable.sol" ;contract ZY is ERC721A, Ownable { event Log(string message, uint number); event Mint(string message, address indexed receiver); uint256 public constant PRICE_PER_TOKEN = 0.0001 ether; string private _resourceURI; address[] private white_list; address[] private free_mint; mapping(address => bool) public minted; constructor ( ) ERC721A ("zy" , "ZY" ) {} modifier gasCheck ( ) { uint cnt = 0 ; for (uint i = 0 ; i < white_list.length; ++i) { if (!minted[white_list[i]]) { cnt += 1 ; } } require (msg.value >= cnt * PRICE_PER_TOKEN, "Not Enough Gas for Mint" ); _; } function mint (uint256 quantity ) external payable onlyOwner ( ) { _safeMint(msg.sender, quantity); } function mintList (address[] storage list ) internal { uint cnt = 0 ; for (uint i = 0 ; i < list.length; ++i) { address current = list[i]; if (!minted[current]) { _safeMint(current, 1 ); minted[current] = true ; emit Mint("Minted to " , current); cnt += 1 ; } } emit Log("Totally Minted " , cnt); } function mint2wl ( ) external payable onlyOwner ( ) gasCheck ( ) { mintList(white_list); } function mint2free ( ) external payable onlyOwner ( ) { mintList(free_mint); } function addWhiteListMember (address wlMember ) external onlyOwner ( ) { white_list.push(wlMember); } function addFreeMintMember (address member ) external onlyOwner ( ) { free_mint.push(member); } function setResourceURI (string calldata resourceURI_ ) external onlyOwner ( ) { _resourceURI = resourceURI_; } function _baseURI ( ) internal view virtual override returns (string memory ) { return _resourceURI; } function kill ( ) external onlyOwner ( ) { selfdestruct(payable(msg.sender)); } }
上述合约的gas报告,这是通过hardhat-gas-reporer 来完成的。实时价格获取是通过coinmarketcap
,需要注册并获得一个免费的API供测试用:
IPFS官方也有相关的教程 可供参考。接下来我们补充合约交互JS脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 async function mint ( ) { const contractWithSigner = contract.connect(wallet); const price = 100 ; console .log("price is " + price); const tx = await contractWithSigner.mint(1 , { value : price}); console .log(tx.hash); await tx.wait(); console .log("mint success" ); } async function kill ( ) { const contractWithSigner = contract.connect(wallet); const tx = await contractWithSigner.kill() console .log(tx.hash); await tx.wait(); console .log("self destruct success" ); } async function setResourceURI ( ) { const contractWithSigner = contract.connect(wallet); const tx = await contractWithSigner.setResourceURI("这里填写你的资源文件URI" ) console .log(tx.hash); await tx.wait(); console .log("setBaseURL success" ); }
Opensea测试网的效果如下:
后记 Opensea Testnets 和Looksrare Rinkeby 都可以查看我们的mint
结果,执行完事务后到上面去看看吧。本篇文章的例子 。