如何用 OpenZeppelin Contracts Base64 将链上 JSON 元数据编码为 NFT 的 Data URI?
【免费下载链接】openzeppelin-contractsOpenZeppelin Contracts is a library for secure smart contract development.项目地址: https://gitcode.com/GitHub_Trending/op/openzeppelin-contracts
用 ERC-721 时,tokenURI通常返回一个指向链下 JSON 文件的 URL。OpenZeppelin 官方 ERC-721 指南指出了这种做法的隐患:元数据不在链上,“a game developer could change the underlying metadata”(游戏开发者可以随时改动底层元数据)。如果希望把所有信息放进链上,指南给出的方案是:让tokenURI返回一个 Base64 编码的 Data URI,同时明确提醒这种全链上元数据的方式 “will be rather costly”(成本相当高)。
这篇文章的目标就是完成这一改造:使用 OpenZeppelin Contracts 的Base64库,把 JSON 元数据编码后拼进data:application/json;base64,...形式的 Data URI,并给出基于仓库自带测试的验证方式。
适用前提
- OpenZeppelin Contracts v5(contracts/utils/Base64.sol 文件头标注 last updated v5.6.0)。
- Solidity 版本:
Base64.sol要求pragma solidity ^0.8.20;官方示例合约 Base64NFT.sol 使用pragma solidity ^0.8.24(因为它同时继承了ERC721),你的项目编译器版本需与之一致。 Base64是一个library:encode与decode都是internal pure函数,只能在你的合约内部调用,没有对外暴露的外部接口。- 场景来自 docs/modules/ROOT/pages/utilities.adoc 的 Base64 章节:该库用于把
bytes转成 Base64string,文档明确说它适合构建 URL-safe 的tokenURI,同时适用于 ERC-721 的tokenURI和 ERC-1155 的uri。
将 JSON 元数据编码为 Data URI
官方文档给出的示例合约Base64NFT完整实现如下(contracts/mocks/docs/utilities/Base64NFT.sol),文档中// ...处省略了你自己的铸造(mint)逻辑,需要自行补齐:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {ERC721} from "../../../token/ERC721/ERC721.sol"; import {Strings} from "../../../utils/Strings.sol"; import {Base64} from "../../../utils/Base64.sol"; contract Base64NFT is ERC721 { using Strings for uint256; constructor() ERC721("Base64NFT", "MTK") {} // ... function tokenURI(uint256 tokenId) public pure override returns (string memory) { // Equivalent to: // { // "name": "Base64NFT #1", // // Replace with extra ERC-721 Metadata properties // } // prettier-ignore string memory dataURI = string.concat("{\"name\": \"Base64NFT #", tokenId.toString(), "\"}"); return string.concat("data:application/json;base64,", Base64.encode(bytes(dataURI))); } }tokenURI里发生了三件事,按顺序理解即可:
- 拼 JSON。
using Strings for uint256让你可以调用tokenId.toString(),用string.concat把 tokenId 拼进 JSON 模板,得到{"name": "Base64NFT #1"}这样的元数据字符串。注释里 “Replace with extra ERC-721 Metadata properties” 表示你可以继续追加image、description等 ERC-721 元数据字段。 - Base64 编码。
Base64.encode(bytes(dataURI))把 JSON 的bytes转成带=填充的标准 Base64 字符串(RFC 4648)。 - 加 Data URI 前缀。
string.concat("data:application/json;base64,", ...)把 MIME 前缀拼在编码结果前面,最终返回值形如data:application/json;base64,eyJ...,整个 URI 不再依赖任何链下服务。
同一个库还有两个可选函数,按需使用:
Base64.encodeURL(bytes memory data):使用 Base64Url 字母表,且按 RFC 4648 规范不加=填充,适合需要 URL-safe 输出的场合。Base64.decode(string memory data):把 Base64 字符串还原为bytes,同时支持带填充和不带填充的输入、两种字母表;遇到非法字符会以InvalidBase64Char(bytes1)回滚,并携带该非法字符。这个函数正好可以用于下一步的验证。
验证结果
运行仓库自带的 Base64 测试
仓库自带两套针对Base64库的测试,可以直接运行来确认库行为。测试在本地临时链上执行,不涉及真实交易,也不修改仓库文件。
Hardhat(主路径,仓库 package.json 中test脚本即运行hardhat test):
npx hardhat test test/utils/Base64.test.jsFoundry(可选,仓库根目录含 foundry.toml 与lib/forge-std):
npx forge test --match-path test/utils/Base64.t.sol测试文件 test/utils/Base64.test.js 中列出了一组期望值(以下是测试文件中的示例结果,不是必须复现的固定基准):
| 输入 | encode期望输出 |
|---|---|
test | dGVzdA== |
test1 | dGVzdDE= |
test12 | dGVzdDEy |
空bytes | 空字符串 |
test/utils/Base64.t.sol 则用 Foundry 断言Base64.encode与vm.toBase64一致,并断言Base64.decode(Base64.encode(input))能还原输入。
验证自己的 tokenURI
上面验证的是库本身;要确认自己合约的tokenURI输出正确,可以在测试里用库自带的encode在本地独立构造一份期望值做对比。以下片段是示例写法,仅使用了仓库已有的 API,1是示例 tokenId,换成你要验证的值:
// 示例:在测试合约中验证 Base64NFT 的 tokenURI Base64NFT base64nft = new Base64NFT(); string memory expected = string.concat( "data:application/json;base64,", Base64.encode('{"name": "Base64NFT #1"}') ); assertEq(base64nft.tokenURI(1), expected);assertEq来自 forge-std(Test基类),若你用 Hardhat 测试则用 chai 的expect(...).to.equal(...)代替即可。两条路径都能通过,说明链上tokenURI返回的就是可直接被钱包/市场解析的 Data URI。
限制与注意
- Gas 成本:官方 ERC-721 指南明确说明把全部元数据放链上 “rather costly”。Data URI 的长度取决于 JSON 大小,
tokenURI每次调用都会重新编码,长元数据会显著推高调用成本。 - 函数可见性:
encode/encodeURL/decode均为internal,无法在链下或外部合约中直接调用Base64.encode,必须像上面那样写进自己的合约。仓库测试里用ethers.deployContract('$Base64')部署库的包装合约(见 test/utils/Base64.test.js)只是为了测试内部函数,这是测试侧的手段。 - decode 的失败行为:输入含非法字符(测试覆盖了
*、{、@三种情况)时decode会 revert 并抛出InvalidBase64Char,携带该字符。 - encode 与 encodeURL 不要混用:两者字母表和填充规则不同(
+//对比-/_,后者无填充),但decode对两种输出都能解码。 - ERC-1155:同样适用——把
Base64.encode的结果拼进uri(uint256)的返回值即可,文档在 utilities 页面同时列出了两个代币标准的入口。
相关文档
- docs/modules/ROOT/pages/utilities.adoc:Base64 章节,Data URI 用途与示例的出处。
- docs/modules/ROOT/pages/erc721.adoc:
tokenURI元数据结构、链下 URL 的问题与链上 Base64 方案的 TIP。 - contracts/utils/Base64.sol:库的完整实现与函数注释。
- contracts/mocks/docs/utilities/Base64NFT.sol:可直接参考的
tokenURI示例合约。
【免费下载链接】openzeppelin-contractsOpenZeppelin Contracts is a library for secure smart contract development.项目地址: https://gitcode.com/GitHub_Trending/op/openzeppelin-contracts
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考