Anchor

使用Rust和Anchor创建代币

本指南演示如何使用RustAnchor框架和Metaplex Token Metadata程序通过CPI在Solana上创建带元数据的同质化代币。

你将构建的内容

一个Anchor指令,可以:

  • 创建新的SPL代币铸币账户
  • 为付款方创建关联代币账户
  • 创建包含名称、符号和URI的元数据账户
  • 向付款方铸造初始代币供应量

概要

使用Anchor (Rust)在Solana上创建同质化SPL代币,铸造初始供应量,并通过CPI附加Metaplex Token Metadata(名称、符号、URI)。

  • 一个指令:初始化铸币账户 + ATA + 元数据,然后铸造供应量
  • 使用:SPL Token + Metaplex Token Metadata CPI
  • 已测试:Anchor 0.32.1, Solana Agave 3.1.6
  • 仅限同质化代币;NFT需要Master Edition + decimals=0 + supply=1

不在本指南范围内

Token-2022扩展、机密转账、权限撤销、元数据更新、完整NFT流程、主网部署。

快速开始

跳转至: 程序 · 测试客户端 · 常见错误

  1. anchor init anchor-spl-token
  2. Cargo.toml中添加带有metadata特性的anchor-spl
  3. Anchor.toml中为localnet克隆Token Metadata程序
  4. 粘贴程序代码并运行anchor test

前置要求

  • 已安装Rustrustup.rs
  • 已安装Solana CLIdocs.solana.com
  • 已安装Anchor CLIcargo install --git https://github.com/coral-xyz/anchor anchor-cli
  • Node.jsYarn用于运行测试
  • 一个有SOL余额的Solana钱包用于支付交易费用

已测试配置

本指南使用以下版本进行了测试:

工具版本
Anchor CLI0.32.1
Solana CLI3.1.6 (Agave)
Rust1.92.0
Node.js22.15.1
Yarn1.22.x

初始设置

首先初始化一个新的Anchor项目:

anchor init anchor-spl-token
cd anchor-spl-token

配置Cargo.toml

更新programs/anchor-spl-token/Cargo.toml

programs/anchor-spl-token/Cargo.toml
1[package]
2name = "anchor-spl-token"
3version = "0.1.0"
4description = "Created with Anchor"
5edition = "2021"
6
7[lib]
8crate-type = ["cdylib", "lib"]
9name = "anchor_spl_token"
10
11[lints.rust]
12unexpected_cfgs = { level = "warn", check-cfg = [
13 'cfg(feature, values("custom-heap", "custom-panic", "anchor-debug"))'
14] }
15
16[features]
17default = []
18cpi = ["no-entrypoint"]
19no-entrypoint = []
20no-idl = []
21no-log-ix-name = []
22idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"]
23
24[dependencies]
25anchor-lang = "0.32.1"
26anchor-spl = { version = "0.32.1", features = ["token", "metadata", "associated_token"] }

重要

idl-build特性必须包含anchor-spl/idl-build,否则会出现类似no function or associated item named 'create_type' found for struct 'anchor_spl::token::Mint'的错误。

配置Anchor.toml

更新Anchor.toml以克隆Token Metadata程序用于本地测试:

Anchor.toml
1[toolchain]
2package_manager = "yarn"
3
4[features]
5resolution = true
6skip-lint = false
7
8[programs.localnet]
9anchor_spl_token = "YOUR_PROGRAM_ID_HERE"
10
11[registry]
12url = "https://api.apr.dev"
13
14[provider]
15cluster = "localnet"
16wallet = "~/.config/solana/id.json"
17
18[scripts]
19test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
20
21[test.validator]
22url = "https://api.mainnet-beta.solana.com"
23bind_address = "127.0.0.1"
24
25[[test.validator.clone]]
26address = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"

  • bind_address = "127.0.0.1" 是Agave 3.x验证器所必需的(0.0.0.0会导致panic)
  • [[test.validator.clone]]部分从主网克隆Metaplex Token Metadata程序

配置package.json

package.json
1{
2 "license": "ISC",
3 "scripts": {
4 "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w",
5 "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check"
6 },
7 "dependencies": {
8 "@coral-xyz/anchor": "^0.32.1",
9 "@metaplex-foundation/mpl-token-metadata": "^3.4.0",
10 "@solana/spl-token": "^0.4.9"
11 },
12 "devDependencies": {
13 "chai": "^4.3.4",
14 "mocha": "^9.0.3",
15 "ts-mocha": "^10.0.0",
16 "@types/bn.js": "^5.1.0",
17 "@types/chai": "^4.3.0",
18 "@types/mocha": "^9.0.0",
19 "typescript": "^5.7.3",
20 "prettier": "^2.6.2"
21 }
22}

程序

导入和模板

这里我们定义所有导入并在programs/anchor-spl-token/src/lib.rs中创建Account结构体和指令的模板:

programs/anchor-spl-token/src/lib.rs
1use anchor_lang::prelude::*;
2use anchor_spl::{
3 associated_token::AssociatedToken,
4 metadata::{
5 create_metadata_accounts_v3, mpl_token_metadata::types::DataV2, CreateMetadataAccountsV3,
6 Metadata,
7 },
8 token::{mint_to, Mint, MintTo, Token, TokenAccount},
9};
10
11declare_id!("YOUR_PROGRAM_ID_HERE");
12
13#[program]
14pub mod anchor_spl_token {
15 use super::*;
16
17 pub fn create_token(
18 ctx: Context<CreateToken>,
19 name: String,
20 symbol: String,
21 uri: String,
22 decimals: u8,
23 amount: u64,
24 ) -> Result<()> {
25 Ok(())
26 }
27}
28
29#[derive(Accounts)]
30#[instruction(name: String, symbol: String, uri: String, decimals: u8)]
31pub struct CreateToken<'info> {
32
33}

创建Account结构体

CreateToken结构体定义了指令所需的所有账户并应用必要的约束:

programs/anchor-spl-token/src/lib.rs
1#[derive(Accounts)]
2#[instruction(name: String, symbol: String, uri: String, decimals: u8)]
3pub struct CreateToken<'info> {
4 #[account(mut)]
5 pub payer: Signer<'info>,
6
7 /// The mint account to be created
8 #[account(
9 init,
10 payer = payer,
11 mint::decimals = decimals,
12 mint::authority = payer.key(),
13 mint::freeze_authority = payer.key(),
14 )]
15 pub mint: Account<'info, Mint>,
16
17 /// The associated token account to receive minted tokens
18 #[account(
19 init,
20 payer = payer,
21 associated_token::mint = mint,
22 associated_token::authority = payer,
23 )]
24 pub token_account: Account<'info, TokenAccount>,
25
26 /// The metadata account to be created
27 /// CHECK: Validated by seeds constraint to be the correct PDA
28 #[account(
29 mut,
30 seeds = [
31 b"metadata",
32 token_metadata_program.key().as_ref(),
33 mint.key().as_ref(),
34 ],
35 bump,
36 seeds::program = token_metadata_program.key(),
37 )]
38 pub metadata_account: UncheckedAccount<'info>,
39
40 pub token_program: Program<'info, Token>,
41 pub token_metadata_program: Program<'info, Metadata>,
42 pub associated_token_program: Program<'info, AssociatedToken>,
43 pub system_program: Program<'info, System>,
44 pub rent: Sysvar<'info, Rent>,
45}

账户类型说明:

  • #[instruction(...)]属性允许在账户约束中使用指令参数(如decimals
  • mint使用Anchor的init约束和mint::decimals = decimals来创建具有指定小数位数的代币铸币账户
  • token_account使用associated_token::辅助宏初始化为关联代币账户
  • metadata_account使用seeds::program验证PDA属于Token Metadata程序

创建指令

create_token函数通过CPI创建元数据账户并铸造初始代币供应量:

programs/anchor-spl-token/src/lib.rs
1pub fn create_token(
2 ctx: Context<CreateToken>,
3 name: String,
4 symbol: String,
5 uri: String,
6 decimals: u8,
7 amount: u64,
8) -> Result<()> {
9 msg!("Creating token mint...");
10 msg!("Mint: {}", ctx.accounts.mint.key());
11 msg!("Creating metadata account...");
12 msg!("Metadata account address: {}", ctx.accounts.metadata_account.key());
13
14 // Cross Program Invocation (CPI) to token metadata program
15 create_metadata_accounts_v3(
16 CpiContext::new(
17 ctx.accounts.token_metadata_program.to_account_info(),
18 CreateMetadataAccountsV3 {
19 metadata: ctx.accounts.metadata_account.to_account_info(),
20 mint: ctx.accounts.mint.to_account_info(),
21 mint_authority: ctx.accounts.payer.to_account_info(),
22 update_authority: ctx.accounts.payer.to_account_info(),
23 payer: ctx.accounts.payer.to_account_info(),
24 system_program: ctx.accounts.system_program.to_account_info(),
25 rent: ctx.accounts.rent.to_account_info(),
26 },
27 ),
28 DataV2 {
29 name,
30 symbol,
31 uri,
32 seller_fee_basis_points: 0,
33 creators: None,
34 collection: None,
35 uses: None,
36 },
37 true, // is_mutable
38 true, // update_authority_is_signer
39 None, // collection_details
40 )?;
41
42 // Mint tokens to the payer's associated token account
43 msg!("Minting {} tokens to {}", amount, ctx.accounts.token_account.key());
44
45 mint_to(
46 CpiContext::new(
47 ctx.accounts.token_program.to_account_info(),
48 MintTo {
49 mint: ctx.accounts.mint.to_account_info(),
50 to: ctx.accounts.token_account.to_account_info(),
51 authority: ctx.accounts.payer.to_account_info(),
52 },
53 ),
54 amount,
55 )?;
56
57 msg!("Token created and {} tokens minted successfully.", amount);
58 Ok(())
59}

该函数执行两次跨程序调用(CPI):

  1. create_metadata_accounts_v3(第14-40行)- 创建并初始化包含名称、符号和URI的元数据账户
  2. mint_to(第43-54行)- 将指定数量的代币铸造到付款方的代币账户

测试客户端

在测试之前,先构建程序:

anchor build

获取您的程序ID并在lib.rsAnchor.toml中更新:

solana address -k target/deploy/anchor_spl_token-keypair.json

然后重新构建并部署:

anchor build
anchor deploy

创建测试

tests/anchor-spl-token.ts创建测试文件:

tests/anchor-spl-token.ts
1import * as anchor from "@coral-xyz/anchor";
2import { Program } from "@coral-xyz/anchor";
3import { AnchorSplToken } from "../target/types/anchor_spl_token";
4import { Keypair, PublicKey, SystemProgram, SYSVAR_RENT_PUBKEY } from "@solana/web3.js";
5import { TOKEN_PROGRAM_ID, ASSOCIATED_PROGRAM_ID } from "@coral-xyz/anchor/dist/cjs/utils/token";
6import { getAssociatedTokenAddressSync } from "@solana/spl-token";
7import { BN } from "bn.js";
8
9const TOKEN_METADATA_PROGRAM_ID = new PublicKey(
10 "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"
11);
12
13describe("anchor-spl-token", () => {
14 const provider = anchor.AnchorProvider.env();
15 anchor.setProvider(provider);
16
17 const program = anchor.workspace.AnchorSplToken as Program<AnchorSplToken>;
18 const payer = provider.wallet;
19
20 it("Creates a token with metadata and mints initial supply", async () => {
21 const mintKeypair = Keypair.generate();
22
23 const tokenName = "My Token";
24 const tokenSymbol = "MYTKN";
25 const tokenUri = "https://example.com/token-metadata.json";
26 const tokenDecimals = 9;
27 const mintAmount = new BN(1_000_000).mul(new BN(10).pow(new BN(tokenDecimals)));
28
29 // Derive the metadata account PDA
30 const [metadataAccount] = PublicKey.findProgramAddressSync(
31 [
32 Buffer.from("metadata"),
33 TOKEN_METADATA_PROGRAM_ID.toBuffer(),
34 mintKeypair.publicKey.toBuffer(),
35 ],
36 TOKEN_METADATA_PROGRAM_ID
37 );
38
39 // Derive the associated token account
40 const tokenAccount = getAssociatedTokenAddressSync(
41 mintKeypair.publicKey,
42 payer.publicKey
43 );
44
45 console.log("Mint address:", mintKeypair.publicKey.toBase58());
46 console.log("Metadata address:", metadataAccount.toBase58());
47 console.log("Token account:", tokenAccount.toBase58());
48
49 const tx = await program.methods
50 .createToken(tokenName, tokenSymbol, tokenUri, tokenDecimals, mintAmount)
51 .accountsPartial({
52 payer: payer.publicKey,
53 mint: mintKeypair.publicKey,
54 tokenAccount: tokenAccount,
55 metadataAccount: metadataAccount,
56 tokenProgram: TOKEN_PROGRAM_ID,
57 tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
58 associatedTokenProgram: ASSOCIATED_PROGRAM_ID,
59 systemProgram: SystemProgram.programId,
60 rent: SYSVAR_RENT_PUBKEY,
61 })
62 .signers([mintKeypair])
63 .rpc();
64
65 console.log("Transaction signature:", tx);
66 console.log("Token created and minted successfully!");
67 });
68});

关键要点:

  • 元数据账户PDA使用以下种子派生:["metadata", TOKEN_METADATA_PROGRAM_ID, mint_pubkey](第29-36行)
  • 关联代币账户使用getAssociatedTokenAddressSync派生(第39-42行)
  • 铸币密钥对必须作为签名者传入,因为它正在被初始化
  • 使用accountsPartial指定账户(Anchor 0.32+语法)
  • 使用BN处理大数字(带小数位的代币数量)
  • tokenDecimals传入指令并用于计算铸造数量

运行测试

yarn install
anchor test

预期输出:

anchor-spl-token
Mint address: GpPyH2FuMcS5PcrKWtrmEkBmW8h8gSwUaxNCQkFXwifV
Metadata address: 6jskfrDAmH9d67iL37CLNBK7Hf6FRwNZbq34q4vGucDq
Token account: J3KCxCfmnK9RJ3onmiUsfBDjvKyuVsAXgWvuypsaFQ2i
Transaction signature: 36v63t5cCsXYM8ny4pgahh...
Token created and minted successfully!
✔ Creates a token with metadata and mints initial supply (243ms)
1 passing (245ms)

元数据JSON格式

uri字段应指向包含代币链下元数据的JSON文件:

token-metadata.json
{
"name": "My Token",
"symbol": "MYTKN",
"description": "A description of my token",
"image": "https://example.com/token-image.png"
}

将此JSON文件托管在永久存储解决方案上,如Arweave或IPFS。

常见错误

no function or associated item named 'create_type' found

在Cargo.toml的idl-build特性中添加"anchor-spl/idl-build"

idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"]

Program account is not executable

在Anchor.toml中克隆Token Metadata程序:

[[test.validator.clone]]
address = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"

UnspecifiedIpAddr(0.0.0.0) / 验证器panic

在Anchor.toml的[test.validator]中添加bind_address = "127.0.0.1"

注意事项

  • amount参数以基本单位表示(包含小数位数)。对于100万个具有9位小数的代币,需传入1_000_000 * 10^9
  • 本示例将铸币权限冻结权限保留在付款方上。生产环境中的代币通常在初始铸造后撤销或转移这些权限。
  • 元数据账户是可变的is_mutable = true)。如果您希望元数据不可变,请设置为false

下一步

  • 部署到Devnet: 在Anchor.toml中将cluster = "devnet"并运行anchor deploy
  • 创建NFT: 设置decimals = 0supply = 1以创建非同质化代币
  • 添加代币扩展: 探索SPL Token 2022了解转账费用、计息代币等功能
  • 了解更多Token Metadata: 查看Token Metadata文档

快速参考

关键程序ID

程序地址
Token ProgramTokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
Associated Token ProgramATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL
Token Metadata ProgrammetaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s
System Program11111111111111111111111111111111

元数据PDA种子

派生元数据PDA

1const [metadataAccount] = PublicKey.findProgramAddressSync(
2 [Buffer.from("metadata"), TOKEN_METADATA_PROGRAM_ID.toBuffer(), mint.toBuffer()],
3 TOKEN_METADATA_PROGRAM_ID
4);

最低依赖

Cargo.toml
anchor-lang = "0.32.1"
anchor-spl = { version = "0.32.1", features = ["token", "metadata", "associated_token"] }

常见问题

术语

  • 同质化代币: decimals >= 0,供应量潜力无限
  • NFT: decimals = 0supply = 1,加上Master Edition账户
  • Token Metadata: Metaplex程序,用于同质化代币和NFT
  • SPL: Solana Program Library,标准代币接口

什么是SPL代币?

SPL代币是Solana上等同于以太坊ERC-20代币的标准。SPL代表Solana Program Library。SPL代币是同质化代币,可以代表货币、治理代币、稳定币或Solana上的任何其他同质化资产。

代币铸币账户和代币账户有什么区别?

  • 代币铸币账户(Token Mint): 创建代币的工厂。它定义了代币的属性(小数位数、供应量、权限)。每种代币类型只有一个铸币账户。
  • 代币账户(Token Account): 持有代币的钱包。每个用户需要为其想持有的每种代币类型创建自己的代币账户。

什么是关联代币账户(ATA)?

关联代币账户是为给定钱包和铸币账户确定性派生的代币账户。ATA不是创建随机的代币账户,而是使用标准派生方式,因此任何人都可以计算出任何钱包的代币账户地址。这是处理代币账户的推荐方式。

什么是Metaplex Token Metadata?

Metaplex Token Metadata是一个将元数据(名称、符号、图像URI)附加到SPL代币的程序。没有它,代币只是匿名的铸币账户。元数据存储在与铸币账户关联的程序派生地址(PDA)中。

为什么本地测试需要克隆Token Metadata程序?

本地Solana测试验证器从干净状态启动,除核心Solana程序外不包含任何程序。Metaplex Token Metadata是部署在主网上的独立程序,因此您需要克隆它才能在本地使用。

我可以使用此代码创建NFT吗?

可以,但需要修改:

  • 设置mint::decimals = 0(NFT不可分割)
  • 仅铸造1个代币
  • 铸造后移除铸币权限(防止创建更多)
  • 添加Master Edition账户(Metaplex NFT标准所需)

在Solana上创建代币需要多少费用?

创建代币需要为三个账户支付租金:

  • 铸币账户:约0.00145 SOL
  • 代币账户:约0.00203 SOL
  • 元数据账户:约0.01 SOL

总计:大约0.015-0.02 SOL(随租金价格而变化)。

Anchor和原生Solana Rust有什么区别?

Anchor是一个通过以下方式简化Solana开发的框架:

  • 自动生成账户序列化/反序列化
  • 通过宏提供声明式账户验证
  • 自动生成TypeScript客户端
  • 处理PDA和CPI等常见模式

原生Solana Rust需要手动处理所有这些问题。

术语表

术语定义
SPL TokenSolana Program Library代币标准,等同于ERC-20
Mint定义代币并可以创建新供应量的账户
Token Account持有特定代币余额的账户
ATA关联代币账户 - 钱包的确定性代币账户
PDA程序派生地址 - 从种子派生的、由程序拥有的地址
CPI跨程序调用 - 从一个Solana程序调用另一个程序
Anchor用于构建Solana程序的Rust框架
MetaplexSolana上NFT和代币元数据的协议
IDL接口定义语言 - 描述程序的接口
Rent在Solana上保持账户存活所需的SOL