3、ERC721元数据标准:链上还是链下?这是个问题
好,咱们进入元数据这块。说实话,ERC721 的元数据扩展接口,是我在 NFT 开发中打交道最多的部分。你想想看,一个 NFT 除了有个 tokenId,总得有点“面子”吧?名字、符号、还有那个指向具体资源的 URI,这些就是元数据的核心。
3.1 元数据扩展接口:name、symbol、tokenURI
ERC721 的元数据扩展,说白了就是给每个 NFT 加了个“身份证”。接口定义很清晰,就三个函数:
/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension
interface IERC721Metadata {
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function tokenURI(uint256 _tokenId) external view returns (string memory);
}
name() 和 symbol() 是集合级别的。比如你的 NFT 项目叫“蓝海探险家”,symbol 就是“BLUE”。这两个值一般不会变,我习惯在构造函数里一次性写死。
tokenURI() 才是重头戏。它返回一个 URI,指向一个 JSON 文件。这个 JSON 文件里描述了该 token 的具体信息。嗯,这里要注意:tokenURI 返回的是字符串,可以是 HTTP 链接,也可以是 IPFS 的 CID,甚至可以是 data URI。
核心要点: tokenURI 返回的是元数据的位置,不是元数据本身。合约只存“指针”,不存“内容”。
3.2 元数据 JSON Schema:标准长什么样?
OpenSea 等平台约定了一套 JSON Schema,虽然不是 ERC721 强制要求的,但大家都这么用。我见过不少项目因为格式不对,导致 NFT 在市场上显示异常。
{
"title": "Asset Metadata",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "标识该资产的名称"
},
"description": {
"type": "string",
"description": "资产的详细描述"
},
"image": {
"type": "string",
"description": "指向资产图片的 URI"
},
"attributes": {
"type": "array",
"description": "资产的属性列表",
"items": {
"type": "object",
"properties": {
"trait_type": { "type": "string" },
"value": { "type": "string" }
}
}
}
}
}
这个 JSON 里,image 字段指向图片,attributes 是属性数组。我在项目中遇到过一个问题:有人把 trait_type 拼写成了 traitType,结果 OpenSea 直接不显示属性。这种坑,踩一次就记住了。
我的建议: 属性值尽量用字符串,别用数字。有些平台对数字类型的解析有 bug,字符串最稳妥。
3.3 链上元数据 vs 链下元数据
这是个老生常谈的话题,但每次讲都有人问。我直接给你对比一下:
| 对比维度 | 链上元数据 | 链下元数据 |
|---|---|---|
| 存储位置 | 直接存在合约里(如 base64 编码的 JSON) | 存在 IPFS、Arweave 或中心化服务器 |
| 不可篡改性 | 完全不可篡改,写入即永恒 | 取决于存储方式。IPFS 有固定 CID,中心化服务器随时可改 |
| Gas 成本 | 极高。存一个 JSON 字符串可能消耗 0.01 ETH 以上 | 极低。只需存一个 URI 字符串 |
| 灵活性 | 差。一旦部署,元数据无法更新 | 好。可以随时更新 URI 指向的内容(中心化方案) |
| 可访问性 | 永远在线,只要以太坊网络存在 | 依赖外部存储的可用性 |
链下元数据:主流选择
绝大多数项目都用链下元数据。为什么?便宜啊!你想想看,一个 NFT 的图片可能几 MB,存到链上得花多少钱?我做过一个项目,5000 个 NFT,如果全存链上,光部署费就得几十万人民币。
链下元数据的典型流程:
- 把图片和 JSON 文件上传到 IPFS
- 在合约的
tokenURI()里返回 IPFS 的 CID - 用户访问时,通过 IPFS 网关获取 JSON,再解析图片
警告: 如果你用中心化服务器存元数据,一旦服务器挂了,你的 NFT 就变成“盲盒”了。我见过一个项目,项目方跑路后服务器关停,所有 NFT 的图片都看不到了。这就是中心化的风险。
链上元数据:极客的选择
链上元数据,说白了就是把整个 JSON 用 base64 编码,直接塞进 tokenURI() 的返回值里。这样返回的 URI 以 data:application/json;base64, 开头。
什么时候用链上?我个人觉得,只有对“永恒性”有极致要求的项目才适合。比如 CryptoPunks 这种,元数据一旦上链,谁也改不了。但代价就是贵,而且图片也得是链上生成的(比如用 SVG)。
// 链上元数据示例(伪代码)
function tokenURI(uint256 tokenId) public view override returns (string memory) {
string memory json = string(abi.encodePacked(
'{',
'"name": "蓝海探险家 #', Strings.toString(tokenId), '",',
'"description": "一个完全链上的 NFT 项目",',
'"image": "data:image/svg+xml;base64,', _getSVGBase64(tokenId), '"',
'}'
));
return string(abi.encodePacked(
'data:application/json;base64,',
Base64.encode(bytes(json))
));
}
这段代码里,我把 SVG 图片也 base64 编码了,整个元数据都在链上。嗯,这样做的好处是“永不丢失”,但每次调用 tokenURI() 都要消耗 gas,因为合约要动态拼接字符串。
3.4 我的选择建议
如果你问我怎么选,我的答案很简单:
- 99% 的项目用链下 + IPFS:便宜、够用、去中心化程度也还行
- 1% 的项目用链上:比如艺术实验、对永恒性有执念的项目
- 千万别用纯中心化服务器:除非你打算跑路,否则别坑用户
我曾经帮一个项目做迁移,他们之前把元数据存在 AWS 上,结果 AWS 账单没付,所有图片都 404 了。用户骂声一片,项目方只能一个个重新上传到 IPFS,然后改合约里的 URI。这种教训,一次就够了。
避坑指南: 如果你用 IPFS,记得用 IPFS 固定服务(如 Pinata、Filebase)。否则你的文件可能因为没人 pin 而被垃圾回收。我习惯在部署脚本里自动 pin 所有元数据,省心。
好了,元数据这块就聊到这儿。下一章咱们讲 ERC1155 的多代币标准,那个更有意思。