本文介绍了如何使用Metaplex元数据标准为SPL代币附加元数据。
我们在之前的教程中介绍了 Metaplex 元数据标准。在本教程中,我们将创建一个 SPL token,并使用 Metaplex 标准将元数据附加到它。
我们将构建一个 Anchor 程序,该程序使用 Metaplex 标准创建带有附加元数据的 SPL token。这允许我们向 token 添加信息,例如名称、符号、图像和其他属性。
在开始构建之前,让我们了解 Metaplex 标准,该标准控制 token 元数据的结构方式。
当我们为 token 创建元数据时,我们需要遵循 Metaplex 定义的特定 JSON 格式。结构取决于我们创建的 token 类型(NFT、同质化 token 等)。
主要有三个标准:
token_standard = 2
)这是带有元数据的常规 SPL token。这是我们将在本文后面创建的示例。
它的元数据 JSON 模式定义如下:
{
"name": "Example Token",
"symbol": "EXT",
"description": "A basic fungible SPL token with minimal metadata.",
"image": "https://example.com/images/ext-logo.png"
}
token_standard = 1
)这类似于 Ethereum 上的 ERC-1155,用于游戏内货币或物品。它被定义为供应量大于 1 但小数位为零(即,没有小数单位)的同质化 SPL token。
它的 JSON 模式包括一些额外的字段,例如 attributes
:
{
"name": "Game Sword",
"description": "A rare in-game sword used in the battle arena.",
"image": "https://example.com/images/sword.png",
"animation_url": "https://example.com/animations/sword-spin.mp4",
"external_url": "https://game.example.com/item/1234",
"attributes": [\
{ "trait_type": "Damage", "value": "12" },\
{ "trait_type": "Durability", "value": "50" }\
],
"properties": {
"files": [\
{\
"uri": "https://example.com/images/sword.png",\
"type": "image/png"\
}\
],
"category": "image"
}
}
token_standard = 0
)这类似于 Ethereum 上的 ERC-721——它代表一个非同质化 Token (NFT)。但是,在 Solana 上,每个 NFT 都是一个单独的 mint,供应量为 1,小数位为 0,而在 Ethereum 上,ERC-721 在单个合约中使用唯一的 token ID。
非同质化标准的 JSON 模式与上面的同质化资产标准相同。这两个标准使用完全相同的元数据结构——区别仅在于链上(供应量和小数位),而不是在 JSON 格式中。
{
"name": "Rare Art Piece",
"description": "A one-of-one digital artwork by Artist X.",
"image": "https://example.com/images/artwork.png",
"animation_url": "https://example.com/animations/artwork-loop.mp4",
"external_url": "https://artistx.example.com/rare-art-piece",
"attributes": [\
{ "trait_type": "Artist", "value": "Artist X" },\
{ "trait_type": "Year", "value": "2025" }\
],
"properties": {
"files": [\
{\
"uri": "https://example.com/images/artwork.png",\
"type": "image/png"\
}\
],
"category": "image"
}
}
注意:在 Ethereum 上,NFT 集合通常存在于一个铸造和管理许多 NFT 的合约中。在 Solana 上,每个 NFT 都是它自己的 mint,集合是通过 Metaplex 元数据中的链上验证链接(我们在之前的教程中介绍的 collection 字段)形成的,而不是通过单个合约。
现在我们了解了这些标准,让我们构建我们的程序来创建一个带有元数据的同质化 token。
这就是我们要完成的:
首先,使用 anchor init spl_with_metadata
创建一个新的 Anchor 项目。
然后,将 Anchor.toml
文件更新为以下内容,以正确配置我们的项目。我们将集群设置为“devnet”,因为我们需要与实际的 Metaplex Token Metadata Program 进行交互,该程序不存在于我们的本地环境中。我们还将添加使用 SPL token 和元数据所需的依赖项:
[toolchain]
package_manager = "yarn"
[features]
resolution = true
skip-lint = false
[programs.localnet]
spl_token_with_metadata = "ApCjqNHgvuvsiQYpX4kGCxXTipcWJUe7NmnNfq3UKrwD"
[registry]
url = "https://api.apr.dev"
[provider]
cluster = "devnet" # 添加了这个
wallet = "~/.config/solana/id.json"
[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
接下来,更新 programs/spl_token_with_metadata/Cargo.toml
文件。
[package]
name = "spl_token_with_metadata"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
name = "spl_token_with_metadata"
[features]
default = []
cpi = ["no-entrypoint"]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] # 添加了 "anchor-spl/idl-build"
[dependencies]
anchor-lang = "0.31.0"
anchor-spl = { version = "0.31.0", features = ["token"] } # 添加了这个
mpl-token-metadata = "5.1.0" # 添加了这个
我们配置我们的项目以使用 Anchor SPL 和 Metaplex Token Metadata crate。
我们添加这些依赖项是为了特定目的:
anchor-spl
: 为 Solana 的 SPL token 程序提供 Anchor 兼容的接口mpl-token-metadata
: 允许我们与 Metaplex 的 Token Metadata Program 交互,以便为我们的 SPL token 创建和管理元数据我们已将 idl-build = ["anchor-spl/idl-build"]
功能添加到我们的 Cargo.toml 中,以便生成一个 IDL 文件,该文件包含 SPL token 类型,允许我们的 TypeScript 客户端正确地与我们的程序交互
现在使用以下代码更新 Anchor 程序。
在这里,我们定义了一个 create_token_metadata
函数,用于将元数据附加到提供的 SPL token。当我们继续进行时,我们将详细解释代码。
// 导入程序所需的依赖项:Anchor、Anchor SPL 和 Metaplex Token Metadata crate
use anchor_lang::prelude::*;
use anchor_spl::token::Mint;
use mpl_token_metadata::instructions::{
CreateMetadataAccountV3Cpi, CreateMetadataAccountV3CpiAccounts,
CreateMetadataAccountV3InstructionArgs,
};
use mpl_token_metadata::types::{Creator, DataV2};
use mpl_token_metadata::ID as METADATA_PROGRAM_ID;
declare_id!("2SZvgGtgotJFy1aKd4Rnm7UEZNxUdP4sdXbeLDgKDiGM"); // 运行 Anchor sync 以更新你的程序 ID
#[program]
pub mod spl_token_with_metadata {
use super::*;
pub fn create_token_metadata(
ctx: Context<CreateTokenMetadata>,
name: String,
symbol: String,
uri: String,
seller_fee_basis_points: u16,
is_mutable: bool,
) -> Result<()> {
// 使用同质化标准格式创建元数据指令参数
// 这遵循我们之前讨论的 token_standard = 2 格式
let data = DataV2 {
name,
symbol,
uri, // 指向带有名称、符号、描述和图像的 JSON
seller_fee_basis_points,
creators: Some(vec![Creator {\
address: ctx.accounts.payer.key(),\
verified: true,\
share: 100,\
}]),
collection: None,
uses: None,
};
// 查找元数据账户地址 (PDA)
let mint_key = ctx.accounts.mint.key();
let seeds = &[\
b"metadata".as_ref(),\
METADATA_PROGRAM_ID.as_ref(),\
mint_key.as_ref(),\
];
let (metadata_pda, _) = Pubkey::find_program_address(seeds, &METADATA_PROGRAM_ID);
// 确保提供的元数据账户与 PDA 匹配
require!(
metadata_pda == ctx.accounts.metadata.key(),
MetaplexError::InvalidMetadataAccount
);
// 创建并执行 CPI 以创建元数据
let token_metadata_program_info = ctx.accounts.token_metadata_program.to_account_info();
let metadata_info = ctx.accounts.metadata.to_account_info();
let mint_info = ctx.accounts.mint.to_account_info();
let authority_info = ctx.accounts.authority.to_account_info();
let payer_info = ctx.accounts.payer.to_account_info();
let system_program_info = ctx.accounts.system_program.to_account_info();
let rent_info = ctx.accounts.rent.to_account_info();
let cpi = CreateMetadataAccountV3Cpi::new(
&token_metadata_program_info,
CreateMetadataAccountV3CpiAccounts {
metadata: &metadata_info,
mint: &mint_info,
mint_authority: &authority_info,
payer: &payer_info,
update_authority: (&authority_info, true),
system_program: &system_program_info,
rent: Some(&rent_info),
},
CreateMetadataAccountV3InstructionArgs {
data,
is_mutable,
collection_details: None,
},
);
cpi.invoke()?;
Ok(())
}
}
我们导入程序所需的依赖项:来自 anchor_spl
crate 的 SPL token 实用程序用于 token 操作,以及来自 mpl_token_metadata
crate 的 Metaplex token 元数据组件,用于创建和构建元数据账户。
create_token_metadata
函数将元数据附加到 SPL token mint。我们将通过使用我们在后面的测试中创建的 mint 地址调用它来演示这一点。
create_token_metadata
函数让我们来看看 create_token_metadata
函数,从最重要的部分开始:
首先,我们使用从 mpl_token_metadata::types
(Metaplex 元数据 crate)导入的 DataV2
struct 类型定义元数据账户的内容。
此结构保存我们的元数据账户的数据,并由 Metaplex 在构建元数据账户时使用。
接下来,我们进行检查以确保为 token(mint)传递了正确的元数据账户。
最后,我们构造并执行一个 CreateMetadataAccountV3
指令(我们在之前的文章中讨论过),通过 CPI 调用 Metaplex Token Metadata Program。
从上面的图片中,我们可以看到我们向 CreateMetadataAccountV3Cpi
传递了几个账户。这些是创建元数据所需的账户。这些传递的账户在下面的上下文结构中定义,我们在其中解释了每个账户的来源和目的。
现在,让我们看一下 create_token_metadata
函数的上下文结构,它包含 CreateMetadataAccountV3Cpi
CPI 调用所需的账户:
下面的 CreateTokenMetadata
账户结构包括:
metadata
: 将由 Metaplex Token Metadata Program 创建的元数据 PDAmint
: 我们将附加元数据的现有 SPL token mint 账户。我们将在测试我们的程序时创建并传递此账户authority
: 用于授权此交易的 mint 权限payer
: 支付交易费用和租金的钱包账户system_program
: 用于创建新账户的 Solana 系统程序rent
: 用于计算租金豁免的 Solana 租金系统变量token_metadata_program
: 链上 Metaplex Token Metadata Program(在我们代码中定义为 METADATA_PROGRAM_ID
的固定地址)我们还定义了一个自定义 MetaplexError
错误来处理验证失败。将此代码添加到程序代码中。
#[derive(Accounts)]
pub struct CreateTokenMetadata<'info> {
/// CHECK: 元数据 PDA(将由 Metaplex Token Metadata 程序通过 create_token_metadata 函数中的 CPI 创建)
#[account(mut)]
pub metadata: AccountInfo<'info>,
// token 的 mint 账户
#[account(mut)]
pub mint: Account<'info, Mint>,
// token的 mint 权限
pub authority: Signer<'info>,
// 为交易付费的账户
#[account(mut)]
pub payer: Signer<'info>,
// 我们的代码依赖的链上程序
pub system_program: Program<'info, System>,
pub rent: Sysvar<'info, Rent>,
/// CHECK: 这是 Metaplex Token Metadata 程序
#[account(address = METADATA_PROGRAM_ID)]
// 约束以确保传递正确的账户
pub token_metadata_program: AccountInfo<'info>,
}
#[error_code]
pub enum MetaplexError {
#[msg("The provided metadata account does not match the PDA for this mint")]
InvalidMetadataAccount,
}
让我们为我们的程序实现这个测试。
Irys(以前称为 Bundlr)是一项服务,可以轻松地将数据上传到 Arweave,这是一种永久的去中心化存储。在我们的测试中,我们将使用 Irys 将我们 token 的图像和元数据 JSON 上传到永久存储。
关于 Irys 的要点:
irysStorage
模块包含在 @metaplex-foundation/js
包中,因此在安装 @metaplex-foundation/js
之后不需要安装额外的依赖项。你可以直接导入它。现在使用下面的代码更新 tests/spl_token_with_metadata.ts
中的程序测试。
该测试执行以下操作:
create_token_metadata
有添加的代码注释以显示每个步骤。
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import {
Metaplex,
irysStorage,
keypairIdentity,
toMetaplexFile,
} from "@metaplex-foundation/js";
import { createMint } from "@solana/spl-token";
import { Keypair, PublicKey, SystemProgram } from "@solana/web3.js";
import { assert } from "chai";
import { readFileSync } from "fs";
import path from "path";
import { SplTokenWithMetadata } from "../target/types/spl_token_with_metadata";
describe("spl_token_with_metadata", () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace
.splTokenWithMetadata as Program<SplTokenWithMetadata>;
const wallet = provider.wallet as anchor.Wallet;
const TOKEN_METADATA_PROGRAM_ID = new PublicKey(
"metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"
);
// 使用 Irys(以前称为 Bundlr)配置 Metaplex
const metaplex = Metaplex.make(provider.connection)
.use(keypairIdentity(wallet.payer))
.use(
irysStorage({
address: "https://devnet.irys.xyz", // Irys 端点
providerUrl: provider.connection.rpcEndpoint,
timeout: 60_000,
})
);
it("creates token with metadata", async () => {
// 创建 mint
const mintKeypair = Keypair.generate();
await createMint(
provider.connection,
wallet.payer,
wallet.publicKey,
wallet.publicKey,
9,
mintKeypair
);
const mintPubkey = mintKeypair.publicKey;
console.log("Mint Pubkey:", mintPubkey.toBase58());
// 读取并将我们的图像转换为 MetaplexFile
const imageBuffer = readFileSync(
path.resolve(__dirname, "../assets/image/kitten.png")
);
const metaplexFile = toMetaplexFile(imageBuffer, "kitten.png");
// 上传图像,获取 arweave URI 字符串
const arweaveImageUri: string = await metaplex.storage().upload(metaplexFile);
const imageTxId = arweaveImageUri.split("/").pop()!;
const imageUri = `https://devnet.irys.xyz/${imageTxId}`;
console.log("Devnet Irys image URL:", imageUri); // 使用 Irys devnet 网关,因为 Arweave 公共网关没有 devnet
// 构建我们的 JSON 元数据对象,遵循同质化标准格式
// 这与我们之前解释的 token_standard = 2 格式匹配
const metadata = {
name: "Test Token",
symbol: "TEST",
description: "Test token with metadata example",
image: imageUri,
};
// 上传 JSON,获取 arweave URI 字符串
const arweaveMetadataUri: string = await metaplex
.storage()
.uploadJson(metadata);
const metadataTxId = arweaveMetadataUri.split("/").pop()!;
const metadataUri = `https://devnet.irys.xyz/${metadataTxId}`;
console.log("Devnet Irys metadata URL:", metadataUri); // 使用 Irys devnet 网关,因为 Arweave 公共网关没有 devnet
// 派生链上元数据 PDA
const [metadataPda] = PublicKey.findProgramAddressSync(
[\
Buffer.from("metadata"),\
TOKEN_METADATA_PROGRAM_ID.toBuffer(),\
mintPubkey.toBuffer(),\
],
TOKEN_METADATA_PROGRAM_ID
);
console.log("Metadata PDA:", metadataPda.toBase58());
// 调用 create_token_metadata 函数
const tx = await program.methods
.createTokenMetadata(
metadata.name,
metadata.symbol,
metadataUri,
100, // 1%
true // isMutable
)
.accounts({
metadata: metadataPda,
mint: mintPubkey,
authority: wallet.publicKey,
payer: wallet.publicKey,
systemProgram: SystemProgram.programId,
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
})
.rpc();
console.log("Transaction signature:", tx);
// 断言账户存在且由 Metadata 程序拥有
const info = await provider.connection.getAccountInfo(metadataPda);
assert(info !== null, "Metadata account must exist");
assert(
info.owner.equals(TOKEN_METADATA_PROGRAM_ID),
"Wrong owner for metadata account"
);
});
});
现在我们已经准备好测试,在我们的工作区中创建一个 assets/image
目录,以放置我们将用作 token 图像的图像。此图像将由 Irys 在我们的测试中使用:
我们已经为你将此图像上传到 Irys devnet。单击下面的链接下载它并将其放置在你刚刚创建的目录中:https://devnet.irys.xyz/8VY89xG1RiUjtz1Lwgip7eUxZvtsdkf1gViGYaDKmwx8
现在,在终端上运行 npm install @solana/spl-token @metaplex-foundation/js
以安装测试的依赖项。
接下来,配置 Solana 以使用 devnet。
在终端上运行此命令:solana config set --url [https://api.devnet.solana.com](https://api.devnet.solana.com/)
请求空投:solana airdrop 5
。我们需要资金来将我们的程序和所有相关账户部署到 devnet。
现在构建项目并运行测试。这会将程序、token 和元数据账户部署到 Solana devnet(注意:这可能需要一些时间,具体取决于你的网络连接)。
我们可以看到 mint(token)已使用元数据(并带有一个图像)创建,如下所示。
我们也可以查看其元数据。
元数据账户:https://explorer.solana.com/address/5feQdhNd3PxPJ9apKUpCWfB47cQdLitNMrVP8Gnq3cad?cluster=devnet
要查看元数据账户的效果,请查看常规 SPL token 部署的 UI(我们在之前的教程中做过)。请注意,顶部没有名称或图像,并且在“历史记录”、“转移”和“指令”选项卡旁边没有“元数据”选项卡。
我们已经成功部署了一个 SPL token,并使用 Metaplex 标准将元数据附加到它,遵循我们之前讨论的同质化标准格式,其中包含基本的名称、符号、描述和图像字段。
在本教程中,我们创建了一个 SPL token,并在 Metaplex Token Metadata 标准的帮助下将元数据附加到它。
在我们的 Anchor 程序中,我们使用 DataV2
struct 定义 token 的元数据,并从 mpl-token-metadata
crate 调用 CreateMetadataAccountV3
指令(通过 CPI)来创建元数据账户。我们使用带有 Irys 的 Metaplex 将 token 的图像和元数据 JSON 上传到 Arweave。然后,我们确认元数据账户在创建后存在并且由 Metaplex 程序拥有。最后,我们解释了 Metaplex token 标准——同质化 (2)、同质化资产 (1) 和非同质化 (0)——并概述了它们的 JSON URI 格式。
本文是 Solana 上的系列教程的一部分。
- 原文链接: rareskills.io/post/add-m...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!