avatar
原创2022/01/17

NFT 开发入门

AI 总结

从零开始入门 NFT 开发,深入解析 ERC-721 标准的核心接口,讲解 tokenId 与 metadata JSON 的绑定机制、tokenURI 的设计规范,结合 OpenZeppelin 合约库完整演示 NFT 项目的创建、铸造与转移流程,是 NFT 开发入门的最佳实践指南。

简介

非同质化代币(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的属性, 由 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 中,并在智能合约中设置。

点击播放