使用anchor框架创建Mint代币
前面一篇中,详细介绍了旧版的SPL代币的命令行,在这篇中,使用anchor框架来创建一个代币程序。
如果spl代币的mint-authority和freeze-authority是mint账户的话,铸币、冻结、解冻等操作是无法通过命令行执行的,只能在程序中操作。
本篇就此方法做介绍。
本系列的源码:https://github.com/jackygu2006/tutorial_legacy_spl_token
创建anchor程序
anchor init legacy_token
配置rust库
打开programs/legacy_token/Cargo.toml
,在[dependencies]添加:
[dependencies]
anchor-lang = {version = "0.30.1", features = ["init-if-needed"]}
anchor-spl = {version = "0.30.1", features = ["metadata", "token", "associated_token"]}
mpl-token-metadata = "3.2.3"
然后设置 idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"]
注意:上述配置的前提是当前开发环境中用的anchor-lang
版本是0.30.1
,如果版本不同,这里需要更改。
anchor程序对不同库的版本兼容性很差,所以一旦各个库确定好了能编译顺利通过的版本后,不要轻易换版本。
创建程序的目录结构
在programs/legacy_token/
目录下,创建以下目录结构和文件(lib.rs是系统自动生成的)
/src/
lib.rs
instructions/
mod.rs
initialize_token.rs
...
states.rs
errors.rs
constants.rs
- lib.rs: 入口程序
- instructions: 指令目录,每条指令是一个命令,如
initialize_token
,mint_token
,burn_token
等。 - state.rs: PDA账户的结构
- errors.rs: 错误枚举数据
- constants.rs: 常量
上面是一般中大型solana程序的目录结构,如果指令数很少,可以都放在lib.rs
文件中。
完善states.rs
在states.rs中加入TokenMetadata
数据结构,用来规范token的元数据。
use anchor_lang::prelude::*;
#[account]
#[derive(Debug)]
pub struct TokenMetadata {
pub name: String,
pub symbol: String,
pub uri: String,
pub decimals: u8,
}
完善errors.rs
这个文件集中保存了各种出错的提示信息,先暂时配置一个错误提示MetadataAccountInitialized
,之后随着程序的壮大,会不断增加。
use anchor_lang::prelude::*;
#[error_code]
pub enum ErrorCode {
#[msg("Metadata account already initialized")]
MetadataAccountInitialized,
}
完善constants.rs
这个文件中保存了一些常量,如MINT_SEEDS
、MAX_SYMBOL_LENGTH
等,先暂时配置如下:
pub const MINT_SEEDS: &str = "mint";
在instructions/initialize_token.rs中实现指令
代码如下:
use anchor_lang::prelude::*;
use crate::{
constants::MINT_SEEDS,
errors::ErrorCode,
states::*,
};
use anchor_spl::{
metadata::{
mpl_token_metadata::types::DataV2,
CreateMetadataAccountsV3,
create_metadata_accounts_v3,
Metadata as Metaplex
},
token::{Mint, Token}
};
#[derive(Accounts)]
#[instruction(metadata_params: Box<TokenMetadata>)]
pub struct InitializeToken<'info> {
/// CHECK: New Metaplex Account being created
#[account(
mut,
constraint = metadata.to_account_info().data_is_empty() @ ErrorCode::MetadataAccountInitialized
)]
pub metadata: UncheckedAccount<'info>,
#[account(
init,
seeds = [MINT_SEEDS.as_bytes(), metadata_params.name.as_bytes(), metadata_params.symbol.as_bytes()],
bump,
payer = payer,
mint::decimals = metadata_params.decimals,
mint::authority = mint,
mint::freeze_authority = mint,
)]
pub mint: Account<'info, Mint>,
#[account(mut)]
pub payer: Signer<'info>,
pub rent: Sysvar<'info, Rent>,
pub system_program: Program<'info, System>,
pub token_program: Program<'info, Token>,
pub token_metadata_program: Program<'info, Metaplex>,
}
impl<'info> InitializeToken<'info> {
pub fn initialize_token(
ctx: Context<InitializeToken>,
metadata_params: Box<TokenMetadata>,
) -> Result<()> {
let _metadata = metadata_params.clone();
let seeds = &[
MINT_SEEDS.as_bytes(),
_metadata.name.as_bytes(),
_metadata.symbol.as_bytes(),
&[ctx.bumps.mint],
];
let signer = [&seeds[..]];
let token_data: DataV2 = DataV2 {
name: metadata_params.name,
symbol: metadata_params.symbol,
uri: metadata_params.uri,
seller_fee_basis_points: 0,
creators: None,
collection: None,
uses: None,
};
let metadata_ctx = CpiContext::new_with_signer(
ctx.accounts.token_metadata_program.to_account_info(),
CreateMetadataAccountsV3 {
payer: ctx.accounts.payer.to_account_info(),
update_authority: ctx.accounts.mint.to_account_info(),
mint: ctx.accounts.mint.to_account_info(),
metadata: ctx.accounts.metadata.to_account_info(),
mint_authority: ctx.accounts.mint.to_account_info(),
system_program: ctx.accounts.system_program.to_account_info(),
rent: ctx.accounts.rent.to_account_info(),
},
&signer,
);
create_metadata_accounts_v3(metadata_ctx, token_data, false, true, None)?;
msg!("Token mint initiazlied successfully.");
Ok(())
}
}
一般一个指令程序分三个部分:
- 引用需要的crates
- 定义账户结构
- 定义方法
账户结构解释
1- metadata账户:是metaplex用于保存metadata的账户,把该账户定义为UncheckedAccount
,并通过/// CHECK
声明账户。
2- 为了防止使用已经定义过的metadata账户,使用约束检验:metadata.to_account_info().data_is_empty() @ ErrorCode::MetadataAccountInitialized
,即如果metadata账户中有数据,则返回错误。
3- mint账户: 我们使用anchor框架来初始化mint账户,在之后的文章中会介绍如何手动创建mint账户。
有两种情况需要手动创建mint账户而不是使用anchor框架自动创建: 第一种情况是在一个指令中超出了anchor自动创建账户的上限,在anchor中,一条指令中最多创建3个账户,如果超过这个限制,需要手动创建。 第二种情况是有特殊需求,比如在创建
Transfer fee
扩展的Token2022代币mint时,因为anchor尚未开发该扩展的自动化创建,也就是没有类似:extensions::metadata_pointer::authority = mint
或extensions::metadata_pointer::metadata_address = mint
的结构化程序时,需要手动创建。(在之后Transfer fee
扩展编程时会介绍)
4- 在创建mint时,需要指定以下参数:
-
seeds: 用于指定生成mint账户的规则,mint账户本质上是PDA账户,也就是一个没有私钥的特殊账户,其地址由seeds生成。
如例子中:
seeds = [MINT_SEEDS.as_bytes(), metadata_params.name.as_bytes(), metadata_params.symbol.as_bytes()]
,说明该mint由标识符MINT_SEEDS
,代币名称和代币符号构成,也就是相同的代币名称和符号会被视为相同的代币。 -
bump: 一般情况下直接写就行,即由anchor自动定义bump。
-
payer: 创建mint账户的付款人,必须指定,而且必须是一个
Signer
类型的账户。 -
指定参数,包括
- mint::decimals: 如果不写,则默认为9;
- mint::authority: 即mint authority,铸造权账户
- mint::freeze_authority: 即冻结、解冻权账户
5- 签名人账户
签名人负责支付创建mint账户的费用,因为在solana上创建每个账户都需要支付租金(rent),这笔租金将由该签名人支付。
签名人的账户类型必须是Signer<'info>
,而且因为要支付费用,所以必须定义为mut
。
在本例子中,签名人的账户名定义为payer
.
6- 系统账户
包括如下几个系统账户:
pub rent: Sysvar<'info, Rent>,
pub system_program: Program<'info, System>,
pub token_program: Program<'info, Token>,
pub token_metadata_program: Program<'info, Metaplex>,
- rent:租金管理账户
- system_program: 系统账户,也就是
111111111111111111111111111111111
- token_program: 代币程序账户,就两种:
- 一种是Legacy代币程序,地址为
TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
- 另一个是Token2022代币程序,地址为
TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
.
- 一种是Legacy代币程序,地址为
- token_metadata_program: 这是
Metaplex
的元数据管理程序地址:metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s
指令方法解释
1- 本指令是创建一个mint,同时初始化metadata账户。因为旧版的spl-token程序没有配置metadata的功能,需要借助Metaplex
库完成,因为anchor已经集成了Metaplex
库,所以只需要用以下代码即可导入:
use anchor_spl::{
metadata::{
mpl_token_metadata::types::DataV2,
CreateMetadataAccountsV3,
create_metadata_accounts_v3,
Metadata as Metaplex
},
token::{Mint, Token}
};
2- 另外,我们希望由mint程序来创建metadata程序,所以需要用mint账户进行签名,调用Metaplex
的初始化metadata账户的CPI调用,所以需要构建mint的签名:
let _metadata = metadata_params.clone();
let seeds = &[
MINT_SEEDS.as_bytes(),
_metadata.name.as_bytes(),
_metadata.symbol.as_bytes(),
&[ctx.bumps.mint],
];
let signer = [&seeds[..]];
注意:在定义mint签名时,确保seeds和mint账户地址定义时的方法一模一样。
3- 创建metadata账户:
方法create_metadata_accounts_v3
的定义如下:
pub fn create_metadata_accounts_v3<'info>(
ctx: CpiContext<'_, '_, '_, 'info, CreateMetadataAccountsV3<'info>>,
data: mpl_token_metadata::types::DataV2,
is_mutable: bool,
update_authority_is_signer: bool,
collection_details: Option<mpl_token_metadata::types::CollectionDetails>
) -> Result<()>
其中ctx定义如下:
let metadata_ctx = CpiContext::new_with_signer(
ctx.accounts.token_metadata_program.to_account_info(),
CreateMetadataAccountsV3 {
payer: ctx.accounts.payer.to_account_info(),
update_authority: ctx.accounts.mint.to_account_info(),
mint: ctx.accounts.mint.to_account_info(),
metadata: ctx.accounts.metadata.to_account_info(),
mint_authority: ctx.accounts.mint.to_account_info(),
system_program: ctx.accounts.system_program.to_account_info(),
rent: ctx.accounts.rent.to_account_info(),
},
&signer,
);
元数据data定义如下:
let token_data: DataV2 = DataV2 {
name: metadata_params.name,
symbol: metadata_params.symbol,
uri: metadata_params.uri,
seller_fee_basis_points: 0,
creators: None,
collection: None,
uses: None,
};
在入口程序lib.rs中调用initialize_token指令
在lib.rs中添加:
pub fn initialize_token(ctx: Context<InitializeToken>, metadata_data: Box<TokenMetadata>) -> Result<()> {
InitializeToken::initialize_token(ctx, metadata_data)
}
至此,程序部分完成。
编译与部署
设置调试环境
- 如果是本地调试
- 在本地终端启动
solana-test-validator
- 在本地终端执行
solana config set --url localhost
- 在
Anchor.yaml
文件中的[provider]部分配置cluster = "localnet"
- 在本地终端启动
- 如果是在devnet上调试
- 在本地终端执行
solana config set --url devnet
- 在
Anchor.yaml
文件中的[provider]部分配置cluster = "devnet"
- 在本地终端执行
注意:参与测试的账户中需要有足够的SOL。
编译
执行:
anchor build
编译完成后,在./target/
目录中会生成(或更新)一系列文件。
部署
执行anchor deploy
或者solana program deploy ./target/deploy/legacy_token.so
写前端测试脚本
1- 准备测试账户
为方便多账户之间测试,在tests
目录下创建一个账户文件:accounts.json
,保存几个账户的publicKey和secretKey。
[
{
"publicKey": "",
"secretKey": [100, ..., 222]
},
{
"publicKey": "",
"secretKey": [100, ..., 222]
}
]
在测试脚本legacy_token.ts
中,定义几个操作账户:
const deployerAccount = anchor.web3.Keypair.fromSecretKey(Uint8Array.from(testAccounts[0].secretKey));
const user1Account = anchor.web3.Keypair.fromSecretKey(Uint8Array.from(testAccounts[1].secretKey));
const user2Account = anchor.web3.Keypair.fromSecretKey(Uint8Array.from(testAccounts[2].secretKey));
const user3Account = anchor.web3.Keypair.fromSecretKey(Uint8Array.from(testAccounts[3].secretKey));
2- 空投SOL
定义一个空投函数,给每个账户一定数量SOL,确保测试时有足够SOL:
const airdropSol = async (accounts: number) => {
if(provider.connection.rpcEndpoint === 'http://localhost:8899' || provider.connection.rpcEndpoint === 'http://127.0.0.1:8899') {
for(let i = 0; i < accounts; i++) {
const account = anchor.web3.Keypair.fromSecretKey(Uint8Array.from(testAccounts[i].secretKey));
const airdropSignature = await provider.connection.requestAirdrop(new anchor.web3.PublicKey(account.publicKey), 10000000000);
await provider.connection.confirmTransaction(airdropSignature, "confirmed");
console.log(`Airdrop to #${i + 1} successfully, signature: ${airdropSignature}`);
}
}
}
3- 写初始化代币的测试脚本
it("Initialize token", async () => {
const metadataParams = {
name: "Solana test token",
symbol: "STT01",
uri: "https://node1.irys.xyz/F7qkMeZGbDwZbbo6E6XkOahuUlGEcXElWgGnH5zm4zQ",
decimals: 9,
};
const mintAccount = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("mint"), Buffer.from(metadataParams.name), Buffer.from(metadataParams.symbol)],
program.programId,
)[0];
const metadataAccountPda = anchor.web3.PublicKey.findProgramAddressSync(
[
Buffer.from("metadata"),
TOKEN_METADATA_PROGRAM_ID.toBuffer(),
mintAccount.toBuffer()
],
TOKEN_METADATA_PROGRAM_ID,
)[0];
const context = {
metadata: metadataAccountPda,
mint: mintAccount,
payer: deployerAccount.publicKey,
tokenProgram: TOKEN_PROGRAM_ID,
tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
};
const tx = await program.methods.initializeToken(metadataParams).accounts(context).signers([deployerAccount]).rpc();
console.log("Your transaction signature", tx);
});
4- 使用anchor test编译+部署+测试
anchor test --skip-local-validator --skip-deploy --skip-build --skip-lint
5- 检查结果
如果一切正常,应该得到一个交易Hash,复制该交易Hash,在区块链浏览器中查询交易细节:
从上图可以看到mint和metadata账户都创建成功。