Solana 60 天课程

2025年02月27日更新 77 人订阅
原价: ¥ 28 限时优惠
专栏简介 开始 Solana - 安装与故障排除 Solana 和 Rust 中的算术与基本类型 Solana Anchor 程序 IDL Solana中的Require、Revert和自定义错误 Solana程序是可升级的,并且没有构造函数 Solidity开发者的Rust基础 Rust不寻常的语法 Rust 函数式过程宏 Rust 结构体与属性式和自定义派生宏 Rust 和 Solana 中的可见性与“继承” Solana时钟及其他“区块”变量 Solana 系统变量详解 Solana 日志、“事件”与交易历史 Tx.origin、msg.sender 和 onlyOwner 在 Solana 中:识别调用者 Solana 计算单元与交易费用介绍 在 Solana 和 Anchor 中初始化账户 Solana 计数器教程:在账户中读写数据 使用 Solana web3 js 和 Anchor 读取账户数据 在Solana中创建“映射”和“嵌套映射” Solana中的存储成本、最大存储容量和账户调整 在 Solana 中读取账户余额的 Anchor 方法:address(account).balance 功能修饰符(view、pure、payable)和回退函数在 Solana 中不存在的原因 在 Solana 上实现 SOL 转账及构建支付分配器 使用不同签名者修改账户 PDA(程序派生地址)与 Solana 中的密钥对账户 理解 Solana 中的账户所有权:从PDA中转移SOL Anchor 中的 Init if needed 与重初始化攻击 Solana 中的多重调用:批量交易与交易大小限制 Solana 中的所有者与权限 在Solana中删除和关闭账户与程序 在 Anchor 中:不同类型的账户 在链上读取另一个锚点程序账户数据 在 Anchor 中的跨程序调用(CPI) SPL Token 的运作方式 使用 Anchor 和 Web3.js 转移 SPL Token Solana 教程 - 如何实现 Token 出售 使用Metaplex实施代币元数据

使用Metaplex实施代币元数据

本文介绍了如何使用Metaplex元数据标准为SPL代币附加元数据。

我们在之前的教程中介绍了 Metaplex 元数据标准。在本教程中,我们将创建一个 SPL token,并使用 Metaplex 标准将元数据附加到它。

我们将构建一个 Anchor 程序,该程序使用 Metaplex 标准创建带有附加元数据的 SPL token。这允许我们向 token 添加信息,例如名称、符号、图像和其他属性。

在开始构建之前,让我们了解 Metaplex 标准,该标准控制 token 元数据的结构方式。

Metaplex Token 标准和 URI 格式

当我们为 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。

实现同质化标准Token

这就是我们要完成的:

  1. 我们将创建一个带有函数的 Anchor 程序,用于将元数据附加到 SPL token
  2. 创建一个 SPL token(mint 账户)
  3. 通过 CPI 使用 Metaplex Token Metadata Program 创建一个元数据账户并将其链接到 token
  4. 将 token URI 及其内容(如 token 图像)存储在永久存储中,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 程序代码

现在使用以下代码更新 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 类型定义元数据账户的内容。

一个 Rust struct,保存 token 的元数据

此结构保存我们的元数据账户的数据,并由 Metaplex 在构建元数据账户时使用。

验证元数据账户

接下来,我们进行检查以确保为 token(mint)传递了正确的元数据账户。

一个获取元数据 PDA 的函数

通过 CPI 创建元数据账户

最后,我们构造并执行一个 CreateMetadataAccountV3 指令(我们在之前的文章中讨论过),通过 CPI 调用 Metaplex Token Metadata Program。

一个创建元数据账户的 CPI 调用

从上面的图片中,我们可以看到我们向 CreateMetadataAccountV3Cpi 传递了几个账户。这些是创建元数据所需的账户。这些传递的账户在下面的上下文结构中定义,我们在其中解释了每个账户的来源和目的。

现在,让我们看一下 create_token_metadata 函数的上下文结构,它包含 CreateMetadataAccountV3Cpi CPI 调用所需的账户:

下面的 CreateTokenMetadata 账户结构包括:

  • metadata: 将由 Metaplex Token Metadata Program 创建的元数据 PDA
  • mint: 我们将附加元数据的现有 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

Irys(以前称为 Bundlr)是一项服务,可以轻松地将数据上传到 Arweave,这是一种永久的去中心化存储。在我们的测试中,我们将使用 Irys 将我们 token 的图像和元数据 JSON 上传到永久存储。

关于 Irys 的要点:

  • 它已经通过 irysStorage 模块包含在 @metaplex-foundation/js 包中,因此在安装 @metaplex-foundation/js 之后不需要安装额外的依赖项。你可以直接导入它。
  • 不需要单独创建账户,因为它使用我们现有的 Solana 钱包进行付款
  • 在 devnet 中,上传费用使用你钱包中的 devnet SOL 支付(我们稍后将通过空投请求 devnet SOL)

现在使用下面的代码更新 tests/spl_token_with_metadata.ts 中的程序测试。

该测试执行以下操作:

  1. 将 Metaplex Token 元数据程序 ID 定义为常量(PDA 派生和 CPI 调用所需)
  2. 使用生成的密钥对账户创建一个 SPL token
  3. 使用 Irys 设置 Metaplex 以将本地存储的图像上传到 Arweave devnet,这将返回已上传图像的 URI
  4. 在测试中创建 JSON 元数据(带有名称、符号、描述和图像 URI)并将其上传到 Arweave
  5. 使用“metadata”前缀、元数据程序 ID 和 mint 地址作为种子派生元数据 PDA
  6. 在我们的 Anchor 程序中调用 create_token_metadata
  7. 最后,它验证元数据账户是否存在并且具有正确的所有者

有添加的代码注释以显示每个步骤。


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 的 Typescript 代码

我们已经为你将此图像上传到 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/)

一个将默认网络切换到测试网的 shell 命令

请求空投:solana airdrop 5。我们需要资金来将我们的程序和所有相关账户部署到 devnet。

在测试网上空投 5 sol

现在构建项目并运行测试。这会将程序、token 和元数据账户部署到 Solana devnet(注意:这可能需要一些时间,具体取决于你的网络连接)。

一个测试,显示成功创建带有元数据的 token

我们可以看到 mint(token)已使用元数据(并带有一个图像)创建,如下所示。

在区块浏览器上带有可见元数据的测试 token

我们也可以查看其元数据。

一个保存 token 元数据的 JSON

此特定部署的链接:https://explorer.solana.com/tx/2c27FRN48fHzzLTA9kV2XXwCEUPEQcWXvfT3k31PhPoEyFNe3bepJ7XxvKwAXekzPaV5nQeCR8mfxAeKqG15QT4Q?cluster=devnet

元数据账户:https://explorer.solana.com/address/5feQdhNd3PxPJ9apKUpCWfB47cQdLitNMrVP8Gnq3cad?cluster=devnet

要查看元数据账户的效果,请查看常规 SPL token 部署的 UI(我们在之前的教程中做过)。请注意,顶部没有名称或图像,并且在“历史记录”、“转移”和“指令”选项卡旁边没有“元数据”选项卡。

缺少元数据的 token 的屏幕截图

我们已经成功部署了一个 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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论