跳转到内容

ERC721 NFT 开发指南

ERC721 NFT 开发指南

NFT(非同质化代币)遵循 ERC721 标准,每个代币都是独一无二的,代表特定数字或实物资产的所有权。本文介绍 ERC721 标准及 NFT 开发实践。

什么是 NFT?

NFT(遵循 ERC721 标准)定义了一个框架,用于制作独一无二且彼此不同的代币(因此被称为非同质化),而流行的 ERC20 标准则定义了”同质化”的代币,这意味着代币都可以互换,并保证具有相同的价值。

“同质化”货币的例子是美元、欧元和日元,而可互换区块链代币的例子是 AAVE、SNX 和 YFI。在这些情况下,1 个同质化的代币等于 1 个同类的另一个代币,就像 1 美元等于 1 美元一样。

然而,NFT/ERC721 是不同的,因为每个代币都是独一无二的,并不代表相同的价值或可互换的项目。由于所有的 NFT 都是独一无二的,它们可以代表:

  • 现实世界资产的代币化所有权(如一块特定的土地)
  • 数字资产的实际所有权(如稀有的数字交易卡)
  • 游戏角色或道具

ERC721 接口

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IERC721 {
// 事件
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
// 查询函数
function balanceOf(address owner) external view returns (uint256 balance);
function ownerOf(uint256 tokenId) external view returns (address owner);
// 转账函数
function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;
function safeTransferFrom(address from, address to, uint256 tokenId) external;
function transferFrom(address from, address to, uint256 tokenId) external;
// 授权函数
function approve(address to, uint256 tokenId) external;
function setApprovalForAll(address operator, bool approved) external;
function getApproved(uint256 tokenId) external view returns (address operator);
function isApprovedForAll(address owner, address operator) external view returns (bool);
}
// ERC721Metadata 扩展
interface IERC721Metadata is IERC721 {
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function tokenURI(uint256 tokenId) external view returns (string memory);
}

使用 OpenZeppelin 实现基础 NFT

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract MyNFT is ERC721URIStorage, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
uint256 public constant MAX_SUPPLY = 10000;
uint256 public constant MINT_PRICE = 0.01 ether;
event NFTMinted(address indexed to, uint256 indexed tokenId, string tokenURI);
constructor(address initialOwner)
ERC721("My NFT Collection", "MNFT")
Ownable(initialOwner)
{}
function mint(address to, string memory tokenURI_) external payable returns (uint256) {
require(msg.value >= MINT_PRICE, "Insufficient payment");
require(_tokenIds.current() < MAX_SUPPLY, "Max supply reached");
_tokenIds.increment();
uint256 newTokenId = _tokenIds.current();
_safeMint(to, newTokenId);
_setTokenURI(newTokenId, tokenURI_);
emit NFTMinted(to, newTokenId, tokenURI_);
return newTokenId;
}
function totalSupply() public view returns (uint256) {
return _tokenIds.current();
}
function withdraw() external onlyOwner {
payable(owner()).transfer(address(this).balance);
}
}

实战案例:龙与地下城角色 NFT

下面是一个完整的示例,展示如何使用 Chainlink VRF 为 NFT 生成随机属性。

在遵循 ERC721 标准的非同质化代币(NFT)中生成随机数一直是智能合约开发者面临的难题。Chainlink VRF 已经在主网上线,基于 Solidity 的智能合约可以无缝生成防篡改的链上随机数,这些随机数可以证明是公平的,并且有密码学证明支持。

角色属性结构

我们要创建一个具有 D&D 角色六大属性的角色:

struct Character {
uint256 strength; // 力量
uint256 dexterity; // 敏捷
uint256 constitution; // 体质
uint256 intelligence; // 智力
uint256 wisdom; // 感知
uint256 charisma; // 魅力
uint256 experience; // 经验
string name; // 名称
}
Character[] public characters;

完整合约实现

pragma solidity ^0.6.6;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@chainlink/contracts/src/v0.6/VRFConsumerBase.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
contract DungeonsAndDragonsCharacter is ERC721, VRFConsumerBase, Ownable {
struct Character {
uint256 strength;
uint256 dexterity;
uint256 constitution;
uint256 intelligence;
uint256 wisdom;
uint256 charisma;
uint256 experience;
string name;
}
Character[] public characters;
bytes32 internal keyHash;
uint256 internal fee;
mapping(bytes32 => string) requestToCharacterName;
mapping(bytes32 => address) requestToSender;
constructor()
public
VRFConsumerBase(VRFCoordinator, LinkToken)
ERC721("DungeonsAndDragonsCharacter", "D&D")
{
keyHash = 0x2ed0feb3e7fd2022120aa84fab1945545a9f2ffc9076fd6156fa96eaff4c1311;
fee = 0.1 * 10**18; // 0.1 LINK
}
// 请求随机角色
function requestNewRandomCharacter(
uint256 userProvidedSeed,
string memory name
) public returns (bytes32) {
require(
LINK.balanceOf(address(this)) >= fee,
"Not enough LINK - fill contract with faucet"
);
bytes32 requestId = requestRandomness(keyHash, fee, userProvidedSeed);
requestToCharacterName[requestId] = name;
requestToSender[requestId] = msg.sender;
return requestId;
}
// Chainlink VRF 回调函数
function fulfillRandomness(bytes32 requestId, uint256 randomNumber)
internal
override
{
uint256 newId = characters.length;
// 从一个随机数中提取六个属性
uint256 strength = ((randomNumber % 100) % 18);
uint256 dexterity = (((randomNumber % 10000) / 100) % 18);
uint256 constitution = (((randomNumber % 1000000) / 10000) % 18);
uint256 intelligence = (((randomNumber % 100000000) / 1000000) % 18);
uint256 wisdom = (((randomNumber % 10000000000) / 100000000) % 18);
uint256 charisma = (((randomNumber % 1000000000000) / 10000000000) % 18);
uint256 experience = 0;
characters.push(
Character(
strength,
dexterity,
constitution,
intelligence,
wisdom,
charisma,
experience,
requestToCharacterName[requestId]
)
);
// 铸造 NFT
_safeMint(requestToSender[requestId], newId);
}
}

部署和使用

Terminal window
# 克隆项目
git clone https://github.com/PatrickAlphaC/dungeons-and-dragons-nft
cd dungeons-and-dragons-nft
npm install
# 设置环境变量
export MNEMONIC='your mnemonic words here'
export RINKEBY_RPC_URL='https://rinkeby.infura.io/v3/YOUR_PROJECT_ID'
# 部署合约
truffle migrate --reset --network rinkeby
# 向合约注入 LINK 代币(用于 VRF 费用)
truffle exec scripts/fund-contract.js --network rinkeby
# 生成角色
truffle exec scripts/generate-character.js --network rinkeby
# 查看角色属性
truffle exec scripts/get-character.js --network rinkeby
# 验证合约
truffle run verify DungeonsAndDragonsCharacter --network rinkeby --license MIT

NFT 元数据标准

NFT 元数据通常存储在链下(IPFS 或中心化服务器),tokenURI 返回元数据 JSON 的链接。

元数据格式

{
"name": "Dragon #1",
"description": "A fearsome dragon with exceptional strength",
"image": "ipfs://QmXxx.../dragon1.png",
"attributes": [
{ "trait_type": "Strength", "value": 95 },
{ "trait_type": "Intelligence", "value": 72 },
{ "trait_type": "Rarity", "value": "Legendary" },
{ "display_type": "boost_number", "trait_type": "Speed Boost", "value": 10 }
]
}

上传元数据到 IPFS

const { NFTStorage, File } = require('nft.storage');
const client = new NFTStorage({ token: process.env.NFT_STORAGE_KEY });
async function uploadMetadata(imageFile, metadata) {
// 上传图片
const imageCid = await client.storeBlob(new Blob([imageFile]));
// 上传元数据
const metadataCid = await client.store({
name: metadata.name,
description: metadata.description,
image: new File([imageFile], 'image.png', { type: 'image/png' }),
attributes: metadata.attributes
});
return `ipfs://${metadataCid.ipnft}/metadata.json`;
}

批量铸造(ERC721A)

ERC721A 是由 Azuki 开发的优化版本,批量铸造时 Gas 效率极高:

import "erc721a/contracts/ERC721A.sol";
contract MyEfficientNFT is ERC721A {
uint256 public constant MAX_SUPPLY = 10000;
constructor() ERC721A("My NFT", "MNFT") {}
// 批量铸造 5 个仅消耗约 1 次铸造的 Gas
function mintBatch(uint256 quantity) external payable {
require(_totalMinted() + quantity <= MAX_SUPPLY, "Exceeds max supply");
_mint(msg.sender, quantity);
}
}

可升级 NFT

使用链上元数据,根据条件动态更新 NFT 属性:

contract DynamicNFT is ERC721URIStorage, Ownable {
// 根据游戏等级返回不同的 tokenURI
function tokenURI(uint256 tokenId) public view override returns (string memory) {
uint256 level = characterLevels[tokenId];
if (level >= 100) {
return string(abi.encodePacked(baseURI, "/legendary/", tokenId.toString()));
} else if (level >= 50) {
return string(abi.encodePacked(baseURI, "/epic/", tokenId.toString()));
} else {
return string(abi.encodePacked(baseURI, "/common/", tokenId.toString()));
}
}
}

常用 NFT 市场交互

const { ethers } = require('ethers');
// 查询 NFT 信息
async function getNFTInfo(contractAddress, tokenId) {
const provider = new ethers.JsonRpcProvider(process.env.RPC_URL);
const nft = new ethers.Contract(contractAddress, ERC721_ABI, provider);
const owner = await nft.ownerOf(tokenId);
const tokenURI = await nft.tokenURI(tokenId);
// 获取元数据
const metadata = await fetch(tokenURI).then(r => r.json());
return { owner, tokenURI, metadata };
}
// 查询地址持有的所有 NFT
async function getNFTsByOwner(contractAddress, ownerAddress) {
const provider = new ethers.JsonRpcProvider(process.env.RPC_URL);
const nft = new ethers.Contract(contractAddress, ERC721_ABI, provider);
// 通过 Transfer 事件过滤
const filter = nft.filters.Transfer(null, ownerAddress);
const events = await nft.queryFilter(filter);
const tokenIds = [...new Set(events.map(e => e.args.tokenId.toString()))];
// 过滤出当前仍属于该地址的代币
const ownedTokens = await Promise.all(
tokenIds.map(async id => {
try {
const owner = await nft.ownerOf(id);
return owner.toLowerCase() === ownerAddress.toLowerCase() ? id : null;
} catch { return null; }
})
);
return ownedTokens.filter(Boolean);
}

总结

NFT(ERC721)开发要点:

  1. 标准接口:实现 IERC721 和 IERC721Metadata 接口
  2. OpenZeppelin:使用 ERC721、ERC721URIStorage 等基础合约
  3. 随机属性:使用 Chainlink VRF 生成可验证的随机数
  4. 元数据:遵循 OpenSea 元数据标准,存储在 IPFS
  5. 批量铸造:考虑使用 ERC721A 优化 Gas 成本
  6. 动态 NFT:根据链上状态动态更新 tokenURI

NFT 生态持续创新,应用场景包括数字艺术、游戏道具、会员资格、现实资产代币化等。