如何使用Anchor创建Core Collection

Last updated January 31, 2026

本指南将演示如何使用mpl-core Rust SDK crate在Solana程序中通过Anchor框架以CPI方式创建Core NFT Collection

什么是Core?

Core使用单账户设计,与替代方案相比降低了铸造成本并改善了Solana网络负载。它还具有灵活的插件系统,允许开发者修改Asset的行为和功能。

在开始之前,让我们先了解Collection:

什么是Collection?

Collection是属于同一系列或组的一组Asset。为了将Asset分组在一起,我们必须首先创建一个Collection Asset,其目的是存储与该Collection相关的任何元数据,例如Collection名称和Collection图片。Collection Asset充当您Collection的封面,也可以存储Collection范围的插件。

前提条件

  • 您选择的代码编辑器(推荐使用带有Rust Analyzer插件Visual Studio Code
  • Anchor 0.30.1或更高版本。

初始设置

在本指南中,我们将使用Anchor,采用单文件方法,其中所有必要的宏都可以在lib.rs文件中找到:

  • declare_id:指定程序的链上地址。
  • #[program]:指定包含程序指令逻辑的模块。
  • #[derive(Accounts)]:应用于结构体,表示指令所需的账户列表。
  • #[account]:应用于结构体,创建特定于程序的自定义账户类型。 注意:您可能需要根据自己的需要修改和移动函数。

初始化程序

首先使用avm(Anchor版本管理器)初始化一个新项目(可选)。要初始化它,请在终端中运行以下命令:

anchor init create-core-collection-example

所需Crate

在本指南中,我们将使用启用了anchor功能的mpl_core crate。要安装它,首先导航到create-core-collection-example目录:

cd create-core-collection-example

然后运行以下命令:

cargo add mpl-core --features anchor

程序

导入和模板

这里我们将定义本指南的所有导入,并在lib.rs文件中创建Account结构体和指令的模板。

use anchor_lang::prelude::*;
use mpl_core::{
ID as MPL_CORE_ID,
instructions::CreateCollectionV2CpiBuilder,
};
declare_id!("C9PLf3qMCVqtUCJtEBy8NCcseNp3KTZwFJxAtDdN1bto");
#[derive(AnchorDeserialize, AnchorSerialize)]
pub struct CreateCollectionArgs {
}
#[program]
pub mod create_core_collection_example {
use super::*;
pub fn create_core_collection(ctx: Context<CreateCollection>, args: CreateCollectionArgs) -> Result<()> {
Ok(())
}
}
#[derive(Accounts)]
pub struct CreateCollection<'info> {
}

创建Args结构体

为了保持函数的组织性并避免过多参数造成的混乱,标准做法是通过结构化格式传递所有输入。这通过定义参数结构体(CreateCollectionArgs)并派生AnchorDeserializeAnchorSerialize来实现,这允许结构体使用NBOR序列化为二进制格式,使其可被Anchor读取。

#[derive(AnchorDeserialize, AnchorSerialize)]
pub struct CreateCollectionArgs {
name: String,
uri: String,
}

在这个CreateCollectionArgs结构体中,nameuri字段作为输入提供,它们将作为用于创建Core CollectionCreateCollectionV2CpiBuilder指令的参数。 注意:由于这是一个专注于Anchor的指南,我们不会在这里介绍如何创建Uri。如果您不确定如何操作,请参考此示例

创建Account结构体

Account结构体是我们定义指令期望的账户以及指定这些账户必须满足的约束的地方。这通过两个关键构造来完成:类型约束 账户类型 每种类型在您的程序中都有特定的用途:

  • Signer:确保账户已签署交易。
  • Option:允许可能提供或不提供的可选账户。
  • Program:验证账户是特定程序。 约束 虽然账户类型处理基本验证,但它们不足以满足程序可能需要的所有安全检查。这就是约束发挥作用的地方。 约束添加了额外的验证逻辑。例如,#[account(mut)]约束确保collectionpayer账户被设置为可变的,这意味着在指令执行期间可以修改这些账户中的数据。
#[derive(Accounts)]
pub struct CreateCollection<'info> {
#[account(mut)]
pub collection: Signer<'info>,
/// CHECK: this account will be checked by the mpl_core program
pub update_authority: Option<UncheckedAccount<'info>>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
#[account(address = MPL_CORE_ID)]
/// CHECK: this account is checked by the address constraint
pub mpl_core_program: UncheckedAccount<'info>,
}

CreateCollection结构体中的某些账户被标记为optional。这是因为在CreateCollectionV2CpiBuilder的定义中,某些账户可以省略。

/// ### Accounts:
///
/// 0. `[writable, signer]` collection
/// 1. `[optional]` update_authority
/// 2. `[writable, signer]` payer
/// 3. `[]` system_program

为了使示例尽可能灵活,程序指令中的每个optional账户在create_core_collection指令的账户结构体中也被视为optional

创建指令

create_core_collection函数利用我们之前定义的CreateCollection账户结构体和CreateCollectionArgs参数结构体的输入来与CreateCollectionV2CpiBuilder程序指令交互。

pub fn create_core_collection(ctx: Context<CreateCollection>, args: CreateCollectionArgs) -> Result<()> {
let update_authority = match &ctx.accounts.update_authority {
Some(update_authority) => Some(update_authority.to_account_info()),
None => None,
};
CreateCollectionV2CpiBuilder::new(&ctx.accounts.mpl_core_program.to_account_info())
.collection(&ctx.accounts.collection.to_account_info())
.payer(&ctx.accounts.payer.to_account_info())
.update_authority(update_authority.as_ref())
.system_program(&ctx.accounts.system_program.to_account_info())
.name(args.name)
.uri(args.uri)
.invoke()?;
Ok(())
}

在这个函数中,CreateCollection结构体中定义的账户通过ctx.accounts访问。在将这些账户传递给CreateCollectionV2CpiBuilder程序指令之前,需要使用.to_account_info()方法将它们转换为原始数据形式。 这种转换是必要的,因为构建器需要以这种格式的账户来正确与Solana运行时交互。 CreateAsset结构体中的某些账户是optional的,意味着它们的值可能是Some(account)None。为了在将这些可选账户传递给构建器之前处理它们,我们使用match语句来检查账户是存在(Some)还是不存在(None),并根据此检查,如果存在则绑定为Some(account.to_account_info()),如果不存在则绑定为None。如下所示:

let update_authority = match &ctx.accounts.update_authority {
Some(update_authority) => Some(update_authority.to_account_info()),
None => None,
};

准备好所有必要的账户后,我们将它们传递给CreateCollectionV2CpiBuilder并使用.invoke()来执行指令,如果需要使用签名者种子则使用.invoke_signed() 有关Metaplex CPI Builder工作原理的更多详细信息,您可以参考此文档

附加操作

在继续之前,如果我们想在创建Asset时就包含插件和/或外部插件,例如FreezeDelegate插件或AppData外部插件呢?以下是我们如何实现。 首先,让我们添加所有必要的额外导入:

use mpl_core::types::{
Plugin, FreezeDelegate, PluginAuthority,
ExternalPluginAdapterInitInfo, AppDataInitInfo,
ExternalPluginAdapterSchema
};

然后让我们创建向量来保存插件和外部插件适配器,这样我们可以使用正确的导入轻松添加插件(或更多):

let mut plugins: Vec<PluginAuthorityPair> = vec![];
plugins.push(
PluginAuthorityPair {
plugin: Plugin::FreezeDelegate(FreezeDelegate {frozen: true}),
authority: Some(PluginAuthority::UpdateAuthority)
}
);
let mut external_plugin_adapters: Vec<ExternalPluginAdapterInitInfo> = vec![];
external_plugin_adapters.push(
ExternalPluginAdapterInitInfo::AppData(
AppDataInitInfo {
init_plugin_authority: Some(PluginAuthority::UpdateAuthority),
data_authority: PluginAuthority::Address{ address: data_authority },
schema: Some(ExternalPluginAdapterSchema::Binary),
}
)
);

最后,让我们将这些插件集成到CreateCollectionV2CpiBuilder程序指令中,如下所示:

CreateCollectionV2CpiBuilder::new(&ctx.accounts.mpl_core_program.to_account_info())
.collection(&ctx.accounts.collection.to_account_info())
.payer(&ctx.accounts.payer.to_account_info())
.update_authority(update_authority.as_ref())
.system_program(&ctx.accounts.system_program.to_account_info())
.name(args.name)
.uri(args.uri)
.plugins(plugins)
.external_plugin_adapters(external_plugin_adapters)
.invoke()?;

注意:如果您不确定应该使用哪些字段和插件,请参考文档

客户端

我们现在已经到达创建Core Collection指南的"测试"部分。但在测试我们构建的程序之前,我们需要编译工作区。使用以下命令构建所有内容,使其准备好进行部署和测试:

anchor build

构建完成后,我们应该部署程序,以便我们可以通过脚本访问它。我们可以在anchor.toml文件中设置要部署程序的集群,然后使用以下命令:

anchor deploy

最后我们准备好测试程序了,但在此之前,我们需要处理tests文件夹中的create_core_collection_example.ts

导入和模板

以下是测试所需的所有导入和通用模板。

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { CreateCoreCollectionExample } from "../target/types/create_core_collection_example";
import { Keypair, SystemProgram } from "@solana/web3.js";
import { MPL_CORE_PROGRAM_ID } from "@metaplex-foundation/mpl-core";
describe("create-core-asset-example", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const wallet = anchor.Wallet.local();
const program = anchor.workspace.CreateCoreCollectionExample as Program<CreateCoreCollectionExample>;
let collection = Keypair.generate();
it("Create Collection", async () => {
});
});

创建测试函数

在测试函数中,我们将定义createCollectionArgs结构体,然后将所有必要的账户传递给createCoreCollection函数。

it("Create Collection", async () => {
let createCollectionArgs = {
name: 'My Collection',
uri: 'https://example.com/my-collection.json',
};
const createCollectionTx = await program.methods.createCoreCollection(createCollectionArgs)
.accountsPartial({
collection: collection.publicKey,
payer: wallet.publicKey,
updateAuthority: null,
systemProgram: SystemProgram.programId,
mplCoreProgram: MPL_CORE_PROGRAM_ID
})
.signers([collection, wallet.payer])
.rpc();
console.log(createCollectionTx);
});

我们首先调用createCoreCollection方法并传入我们刚创建的createCollectionArgs结构体作为输入:

await program.methods.createCoreCollection(createCollectionArgs)

接下来,我们指定函数所需的所有账户。由于其中一些账户是optional的,为了简单起见,我们可以在不需要该账户的地方传入null

.accountsPartial({
collection: collection.publicKey,
payer: wallet.publicKey,
updateAuthority: null,
systemProgram: SystemProgram.programId,
mplCoreProgram: MPL_CORE_PROGRAM_ID
})

最后,我们提供签名者并使用.rpc()方法发送交易:

.signers([collection, wallet.payer])
.rpc();