跳到主要内容

NFT开发入门

· 阅读需 5 分钟

简介

非同质化代币(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;

通常在智能合约中不直接存储文件地址,而是通过 baseURItokenId 拼接方式返回文件地址。这样做的原因是因为每个 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

nftmetadata json 是用来描述nft 的基本信息,并且 opensea 等交易平台也是通过读取该信息用来展示相关数据。

metadata json分为两种:

  • 单个 nftmetadata 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
  • descriptionnft描述
  • attributes: nft的属性, 由 key-value 构成的数组

在智能合约中通过 mapping 类型存储

contract NFT {
// nft 编号(通常从1开始)
uint public tokenIds;

// tokenID 对应的 json 文件地址
mapping(uint256 => string) public _tokenURIs;
}

NFT 合集 metadata

示例:

{
"name": "",
"desciption": "",
"image": "",
"external_link": "",
"seller_fee_basis_points": 100,
"fee_recipient": ""
}

json 文件主要有以下属性:

  • name:合集名称
  • image:合集的封面
  • description:合集描述
  • external_link:外部链接
  • seller_fee_basis_points: 交易费用 100 表示 1%
  • fee_recipient:交易费接收地址

在智能合约中通过函数读取

contract NFT {
function contractURI() public pure returns(string memory) {
returns "xxx.json"
}
}

配置文件通常会传到 ipfs 中,并在智能合约中设置。