简介
非同质化代币(Non-Fungible Token)简称 NFT。遵循着ERC721
的代币标准。
不同于 ERC20
中所有的代币都具有相同的价值和属性。ERC721
中每一枚代币都是独一无二的。为了实现这种独一无二,每一枚代币都有 tokenId
与其对应,并且每一枚代币都有着不同的属性,这种属性使用 metadata json
来描述。如下代码所示:
// nft 代币名称
string private _name;
// nft 代币符号
string private _symbol;
// 存储 tokenID 对应的 拥有者地址
mapping(uint256 => address) private _owners;
// 存储 tokenID 对应的 metadata json 文件的地址
mapping(uint256 => string) private _tokenURIs;
通常在智能合约中不直接存储文件地址,而是通过 baseURI
和tokenId
拼接方式返回文件地址。这样做的原因是因为每个 tokenId
都需要一个对应的属性文件,如果直接存储文件地址的话,无疑会占据大量的存储空间,耗费巨量的 gas
目前普遍的做法是将 NFT
的属性文件命名为对应的 {tokenId}
,并将这些文件统一放到一个文件夹中,同时将文件夹上传到 IPFS
中。访问的时候只需要通过 ipfs://{hash}/{tokenId}
就能访问到文件。例如访问 tokenId
为 10 的属性文件,只需要访问 ipfs://{hash}/10
即可。而在智能合约中却只需要存储一个 baseURI
,也就是 ipfs://{hash}/
当读取链上的 tokenId
对应的属性文件地址的时候是通过调用智能合约中的 tokenURI
方法进行返回
function tokenURI(uint256 tokenId) public override view returns(string memory) {
require(_exists(tokenId), 'token not minted');
string memory base = baseURI;
// 存在 baseURI 则进行拼接
return bytes(base).length > 0 ? string(abi.encodePacked(base, tokenId.toString())) : "";
}
简单实现
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
contract Robo is ERC721URIStorage {
using Counters for Counters.Counter;
using Strings for uint256;
// tokenID 每次 mint 成功时 该值+1
Counters.Counter private _tokenIds;
string public baseURI;
constructor(string memory _baseURI) ERC721("Robo", "RB") {
// tokenID 通常从 1 开始
_tokenIds.increment();
baseURI = _baseURI;
}
// to 为 mint 地址
// tokenURI 为该 nft 的属性描述文件
function mint(address to) public returns (uint256) {
// 获取当前 tokenIds 的值
uint256 newItemId = _tokenIds.current();
// mint 函数
_mint(to, newItemId);
// 自增1
_tokenIds.increment();
return newItemId;
}
function setBaseURI(string memory _baseURI) external {
baseURI = _baseURI;
}
function tokenURI(uint256 tokenId) public override view returns(string memory) {
require(_exists(tokenId), 'token not minted');
string memory base = baseURI;
// 存在 baseURI 则进行拼接
return bytes(base).length > 0 ? string(abi.encodePacked(base, tokenId.toString())) : "";
}
}
_mint
函数为 openzeppelin
内部实现,代码如下
function _mint(address to, uint256 tokenId) internal virtual {
require(to != address(0), "ERC721: mint to the zero address");
require(!_exists(tokenId), "ERC721: token already minted");
// to 地址的余额 + 1
_balances[to] += 1;
// tokenID对应的 NFT 拥有者为 to
_owners[tokenId] = to;
// 触发 Transfer 事件
emit Transfer(address(0), to, tokenId);
}
metadata
nft
的 metadata json
是用来描述nft
的基本信息,并且 opensea
等交易平台也是通过读取该信息用来展示相关数据。
metadata json
分为两种:
- 单个
nft
的metadata json
nft
合集的metadata json
单 NFT metadata
示例:
{
"name": "Robo #1",
"image": "",
"description": "",
"attributes": [
{
"trait_type": "LV",
"value": "SSR"
},
{
"trait_type": "DNA",
"value": "HUMAN"
},
{
"trait_type": "EYE COLOR",
"value": "RED"
}
]
}
json
文件主要有以下属性:
name
:该nft
的名称 通常会加上该nft
的编号image
:该nft
的图片链接,通常存储在IPFS
中description
:nft
描述attributes
:nft