4. Token Metadata 标准详解

Token Metadata 标准详解

1. 标准概述

Token Metadata 是 Metaplex 最早也是最成熟的 NFT 标准,广泛应用于 Solana 生态。几乎你在 Magic Eden 或 Tensor 上看到的所有 NFT,底层都是基于这个标准构建的。

它通过在 SPL Token 之上添加多个 PDA (程序派生地址) 账户来实现丰富的元数据功能。

核心设计思想是 "增强而非替代"

  • 底层依然是标准的 SPL Token,保证了与所有钱包和 DeFi 协议的兼容性。
  • 通过“外挂”账户(Metadata, Master Edition)提供 NFT 特有的能力(如图片、版税、稀缺性)。

2. 账户体系详解

让我们深入代码底层,看看这些账户在 Rust 结构体中到底长什么样。

Metadata Account (元数据账户)

这是最基础的账户,任何代币(包括 USDC 这样的同质化代币)都可以有。

rust
pub struct Metadata { pub key: Key, // 账户类型标识 (用于程序区分这是 Metadata 还是其他账户) pub update_authority: Pubkey, // 元数据管理员:谁有权修改名字或 URI? pub mint: Pubkey, // 关联的 Mint 地址:这张身份证属于哪个代币? pub data: Data, // 核心数据包 (见下文) pub primary_sale_happened: bool, // 是否已完成一级市场销售 (影响版税分配) pub is_mutable: bool, // 是否可变 (如果设为 false,则元数据永久锁定,无法修改) pub edition_nonce: Option<u8>, // 用于生成 Edition PDA 的 nonce pub token_standard: Option<TokenStandard>, // 标准类型 (NonFungible, Fungible, etc.) pub collection: Option<Collection>, // 所属集合 (关键字段!) pub uses: Option<Uses>, // 可消耗性 (例如:这瓶药水可以使用 3 次) } pub struct Data { pub name: String, // 名称 (最大 32 字节,如 "DeGods #123") pub symbol: String, // 符号 (最大 10 字节,如 "DG") pub uri: String, // 链下数据链接 (最大 200 字节,指向 Arweave/IPFS) pub seller_fee_basis_points: u16, // 版税 (基点制,500 = 5%) pub creators: Option<Vec<Creator>>, // 创作者列表 (版税分给谁?) }

Master Edition Account (原版证明)

只有满足 NFT 条件(Supply=1, Decimals=0)的代币才能创建这个账户。它是 NFT 的“身份证”,证明了稀缺性。

rust
pub struct MasterEditionV2 { pub key: Key, pub supply: u64, // 已经从这个原版“打印”了多少个副本? pub max_supply: Option<u64>, // 最多允许打印多少个? // Some(0) = 独版 NFT (1/1),不允许打印副本。 }

Edition Account (副本证明)

如果一个 NFT 是从 Master Edition “打印”出来的(比如限量版海报,印 100 张),它会拥有一个 Edition Account 而不是 Master Edition Account。

rust
pub struct Edition { pub key: Key, pub parent: Pubkey, // 指向它的“父亲” (Master Edition Mint) pub edition: u64, // 编号:这是第几号副本?(如 #1, #2...) }

3. 创作者验证机制 (Creator Verification)

NFT 的价值很大程度上取决于是谁发行的。如果我可以随便填一个 creators 列表,把 Beeple 写进去,岂不是可以伪造名作?

Metaplex 设计了 verified 字段来解决这个问题。

rust
pub struct Creator { pub address: Pubkey, // 创作者钱包地址 pub verified: bool, // 关键字段:该地址是否已经签名确认? pub share: u8, // 版税分成比例 (所有创作者之和必须为 100) }
  • 未验证 (False):当你铸造 NFT 时,你可以把 Beeple 的地址填入 creators,但 verified 默认为 false。市场会显示“未验证创作者”,以此警告买家。
  • 已验证 (True):只有当对应的创作者用私钥对元数据进行签名后,这个字段才会变为 true。这意味着 Beeple 真的承认了这个作品。

4. 实战:查询 NFT 元数据

在客户端获取 NFT 的完整信息通常分两步:

  1. 链上读取:获取 Metadata Account,拿到 uri
  2. 链下抓取:访问 uri (HTTP GET),获取图片、属性等详细 JSON。
typescript
import { Connection, PublicKey } from '@solana/web3.js'; import { Metadata, PROGRAM_ID } from '@metaplex-foundation/mpl-token-metadata'; async function fetchNftMetadata(connection: Connection, mintAddress: string) { const mint = new PublicKey(mintAddress); // 1. 推导 Metadata PDA 地址 const [metadataPda] = PublicKey.findProgramAddressSync( [ Buffer.from('metadata'), PROGRAM_ID.toBuffer(), // metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s mint.toBuffer(), ], PROGRAM_ID ); // 2. 获取链上账户数据 const accountInfo = await connection.getAccountInfo(metadataPda); if (!accountInfo) throw new Error('Metadata account not found'); // 3. 反序列化 (使用 Metaplex SDK 提供的类) const metadata = Metadata.deserialize(accountInfo.data)[0]; // 清理字符串中的空字符 (Rust String 定长填充的 \0) const uri = metadata.data.uri.replace(/\0/g, ''); console.log("🔗 Found Metadata URI:", uri); // 4. 获取链下 JSON const offChainResponse = await fetch(uri); const offChainData = await offChainResponse.json(); return { onChain: metadata, offChain: offChainData }; }

5. 实战:推导所有相关账户

作为一个 NFT 开发者,你经常需要计算各种 PDA。以下是一个工具函数,用于一次性计算 NFT 所有的衍生地址。

typescript
import { PublicKey } from '@solana/web3.js'; const METAPLEX_PROGRAM_ID = new PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s'); function deriveNftAddresses(mintAddress: string) { const mint = new PublicKey(mintAddress); // 1. Metadata PDA const [metadata] = PublicKey.findProgramAddressSync( [Buffer.from('metadata'), METAPLEX_PROGRAM_ID.toBuffer(), mint.toBuffer()], METAPLEX_PROGRAM_ID ); // 2. Master Edition PDA const [masterEdition] = PublicKey.findProgramAddressSync( [ Buffer.from('metadata'), METAPLEX_PROGRAM_ID.toBuffer(), mint.toBuffer(), Buffer.from('edition') // 区别在这里 ], METAPLEX_PROGRAM_ID ); // 3. Token Record PDA (用于可编程 NFT / pNFT) // pNFT 需要知道持有者的 Token Account 才能计算规则 // 这里假设我们要找某个 tokenAccount 对应的记录 const deriveTokenRecord = (tokenAccount: PublicKey) => { return PublicKey.findProgramAddressSync( [ Buffer.from('metadata'), METAPLEX_PROGRAM_ID.toBuffer(), mint.toBuffer(), Buffer.from('token_record'), tokenAccount.toBuffer(), ], METAPLEX_PROGRAM_ID )[0]; }; return { mint, metadata, masterEdition, deriveTokenRecord }; }
JSPlayground
EDITOR ACTIVE
Initializing JS Environment...

Metaplex 宇宙架构

Mint
Auth: Wallet
Metadata PDA
seeds: ['metadata', pid, mint]
Contains: Name, URI
Master Edition
seeds: [..., 'edition']
Enforces: Supply=1
Token Record (pNFT)
seeds: [..., 'token_record']
Token Account Frozen