跳转到内容

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
dateUnix 时间戳,显示为日期
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;
}

深入阅读