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: MITpragma 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: MITpragma 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); }}部署和使用
# 克隆项目git clone https://github.com/PatrickAlphaC/dungeons-and-dragons-nftcd dungeons-and-dragons-nftnpm 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 MITNFT 元数据标准
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 };}
// 查询地址持有的所有 NFTasync 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)开发要点:
- 标准接口:实现 IERC721 和 IERC721Metadata 接口
- OpenZeppelin:使用 ERC721、ERC721URIStorage 等基础合约
- 随机属性:使用 Chainlink VRF 生成可验证的随机数
- 元数据:遵循 OpenSea 元数据标准,存储在 IPFS
- 批量铸造:考虑使用 ERC721A 优化 Gas 成本
- 动态 NFT:根据链上状态动态更新 tokenURI
NFT 生态持续创新,应用场景包括数字艺术、游戏道具、会员资格、现实资产代币化等。