NFT 元数据
NFT 元数据
本页内容正在整理中,欢迎贡献。
概述
NFT 元数据定义了每个 Token 的属性——名称、描述、图片、特征等。元数据的存储和格式直接影响 NFT 在 OpenSea 等市场的显示效果。本文介绍元数据标准、存储方案和最佳实践。
主要内容
元数据 JSON 标准
ERC-721 和 ERC-1155 元数据遵循 OpenSea 定义的事实标准:
{ "name": "My NFT #42", "description": "这是一个描述,支持 Markdown 格式。", "image": "ipfs://QmXxxx.../42.png", "external_url": "https://myproject.io/nft/42", "animation_url": "ipfs://QmXxxx.../42.mp4", "background_color": "FFFFFF", "attributes": [ { "trait_type": "Background", "value": "Ocean Blue" }, { "trait_type": "Level", "value": 5, "display_type": "number", "max_value": 100 }, { "trait_type": "Created Date", "value": 1704067200, "display_type": "date" }, { "trait_type": "Boost Multiplier", "value": 10, "display_type": "boost_number" }, { "trait_type": "Stamina Increase", "value": 10, "display_type": "boost_percentage" } ]}display_type 类型说明:
| display_type | 显示方式 |
|---|---|
| 未设置(字符串) | 字符串属性 |
number | 数字,可设置 max_value |
date | Unix 时间戳,显示为日期 |
boost_number | 正/负数字加成 |
boost_percentage | 百分比加成 |
tokenURI 实现方式
方式一:Base URI + tokenId(批量元数据,最常用)
function _baseURI() internal view override returns (string memory) { return "ipfs://QmCollectionMetadataFolder/";}// tokenURI(1) → "ipfs://QmXxx.../1"(对应 IPFS 上的 1.json 文件)// tokenURI(2) → "ipfs://QmXxx.../2"方式二:每个 Token 独立 URI
mapping(uint256 => string) private _tokenURIs;
function setTokenURI(uint256 tokenId, string memory uri) external onlyOwner { _tokenURIs[tokenId] = uri;}
function tokenURI(uint256 tokenId) public view override returns (string memory) { return _tokenURIs[tokenId];}方式三:链上生成元数据(完全去中心化)
import "@openzeppelin/contracts/utils/Base64.sol";import "@openzeppelin/contracts/utils/Strings.sol";
function tokenURI(uint256 tokenId) public view override returns (string memory) { string memory svg = generateSVG(tokenId); string memory json = Base64.encode(bytes(string(abi.encodePacked( '{"name":"On-Chain NFT #', Strings.toString(tokenId), '",', '"description":"完全存储在区块链上的 NFT",', '"image":"data:image/svg+xml;base64,', Base64.encode(bytes(svg)), '",', '"attributes":[{"trait_type":"ID","value":', Strings.toString(tokenId), '}]}' )))); return string(abi.encodePacked("data:application/json;base64,", json));}
function generateSVG(uint256 tokenId) internal pure returns (string memory) { return string(abi.encodePacked( '<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">', '<rect width="200" height="200" fill="#', uint2hex(tokenId % 0xFFFFFF), '"/>', '<text x="100" y="100" text-anchor="middle" fill="white">#', Strings.toString(tokenId), '</text>', '</svg>' ));}上传元数据到 IPFS
使用 NFT.Storage(免费):
import { NFTStorage, File } from "nft.storage";import fs from "fs";import path from "path";
const client = new NFTStorage({ token: process.env.NFT_STORAGE_TOKEN! });
async function uploadMetadata(tokenId: number, imagePath: string) { const imageFile = new File( [fs.readFileSync(imagePath)], path.basename(imagePath), { type: "image/png" } );
const metadata = await client.store({ name: `My NFT #${tokenId}`, description: `这是第 ${tokenId} 号 NFT`, image: imageFile, attributes: [ { trait_type: "ID", value: tokenId }, ], });
return metadata.url; // ipfs://Qm.../metadata.json}批量上传(使用 Pinata):
import PinataSDK from "@pinata/sdk";import fs from "fs";
const pinata = new PinataSDK(process.env.PINATA_API_KEY!, process.env.PINATA_SECRET!);
// 1. 上传图片文件夹const imageResult = await pinata.pinFromFS("./images");const imageFolderCID = imageResult.IpfsHash;
// 2. 生成并上传元数据文件夹const metadataDir = "./metadata";for (let i = 0; i < TOTAL_SUPPLY; i++) { const metadata = { name: `My NFT #${i}`, description: "NFT 描述", image: `ipfs://${imageFolderCID}/${i}.png`, attributes: generateAttributes(i), }; fs.writeFileSync(`${metadataDir}/${i}`, JSON.stringify(metadata));}
const metadataResult = await pinata.pinFromFS(metadataDir);const baseURI = `ipfs://${metadataResult.IpfsHash}/`;Reveal 机制
防止人们在铸造前通过 tokenURI 预知稀有度:
bool public revealed = false;string public unrevealedURI = "ipfs://QmPreReveal/hidden.json";
function tokenURI(uint256 tokenId) public view override returns (string memory) { if (!revealed) { return unrevealedURI; } return string(abi.encodePacked(_baseURI(), Strings.toString(tokenId)));}
function reveal(string memory finalBaseURI) external onlyOwner { revealed = true; _baseTokenURI = finalBaseURI;}深入阅读
- OpenSea 元数据标准
- ERC-721 实现
- 链上存储 —— IPFS、Arweave 详解