Anchor
使用Rust和Anchor创建代币
本指南演示如何使用Rust、Anchor框架和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流程、主网部署。
快速开始
anchor init anchor-spl-token- 在
Cargo.toml中添加带有metadata特性的anchor-spl - 在
Anchor.toml中为localnet克隆Token Metadata程序 - 粘贴程序代码并运行
anchor test
前置要求
- 已安装Rust(rustup.rs)
- 已安装Solana CLI(docs.solana.com)
- 已安装Anchor CLI(
cargo install --git https://github.com/coral-xyz/anchor anchor-cli) - Node.js和Yarn用于运行测试
- 一个有SOL余额的Solana钱包用于支付交易费用
已测试配置
本指南使用以下版本进行了测试:
| 工具 | 版本 |
|---|---|
| Anchor CLI | 0.32.1 |
| Solana CLI | 3.1.6 (Agave) |
| Rust | 1.92.0 |
| Node.js | 22.15.1 |
| Yarn | 1.22.x |
初始设置
首先初始化一个新的Anchor项目:
anchor init anchor-spl-token
cd 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程序用于本地测试:
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
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结构体和指令的模板:
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结构体定义了指令所需的所有账户并应用必要的约束:
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创建元数据账户并铸造初始代币供应量:
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):
create_metadata_accounts_v3(第14-40行)- 创建并初始化包含名称、符号和URI的元数据账户mint_to(第43-54行)- 将指定数量的代币铸造到付款方的代币账户
测试客户端
在测试之前,先构建程序:
anchor build
获取您的程序ID并在lib.rs和Anchor.toml中更新:
solana address -k target/deploy/anchor_spl_token-keypair.json
然后重新构建并部署:
anchor build
anchor deploy
创建测试
在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文件:
{
"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 = 0和supply = 1以创建非同质化代币 - 添加代币扩展: 探索SPL Token 2022了解转账费用、计息代币等功能
- 了解更多Token Metadata: 查看Token Metadata文档
快速参考
关键程序ID
| 程序 | 地址 |
|---|---|
| Token Program | TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA |
| Associated Token Program | ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL |
| Token Metadata Program | metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s |
| System Program | 11111111111111111111111111111111 |
元数据PDA种子
派生元数据PDA
1const [metadataAccount] = PublicKey.findProgramAddressSync(
2 [Buffer.from("metadata"), TOKEN_METADATA_PROGRAM_ID.toBuffer(), mint.toBuffer()],
3 TOKEN_METADATA_PROGRAM_ID
4);
最低依赖
anchor-lang = "0.32.1"
anchor-spl = { version = "0.32.1", features = ["token", "metadata", "associated_token"] }
常见问题
术语
- 同质化代币:
decimals >= 0,供应量潜力无限 - NFT:
decimals = 0,supply = 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 Token | Solana Program Library代币标准,等同于ERC-20 |
| Mint | 定义代币并可以创建新供应量的账户 |
| Token Account | 持有特定代币余额的账户 |
| ATA | 关联代币账户 - 钱包的确定性代币账户 |
| PDA | 程序派生地址 - 从种子派生的、由程序拥有的地址 |
| CPI | 跨程序调用 - 从一个Solana程序调用另一个程序 |
| Anchor | 用于构建Solana程序的Rust框架 |
| Metaplex | Solana上NFT和代币元数据的协议 |
| IDL | 接口定义语言 - 描述程序的接口 |
| Rent | 在Solana上保持账户存活所需的SOL |
