Token2022 - 创建Mint代币工厂
本篇介绍创建一个Token2022代币,并配置metadata。
本系列的源码:https://github.com/jackygu2006/tutorial_spl_token_2022
创建一个新的anchor项目
参见之前文章,创建一个新的Anchor项目。
创建Mint指令
方法一(简易法)
Anchor框架提供了一种创建Token2022 Mint的方法,较简单,参见以下指令:
该方法将metadata的pointer指向mint账户自己,并在mint账户的存储区内写入metadata,而mint账户的创建,由Anchor框架自动完成,即:
/// CHECK: Defined the mint account by Anchor framework.
#[account(
init,
payer = payer,
seeds = [MINT_SEEDS.as_bytes(), metadata_params.name.as_bytes(), metadata_params.symbol.as_bytes()],
bump,
mint::token_program = token_program,
mint::decimals = 0,
mint::authority = mint,
mint::freeze_authority = mint,
extensions::metadata_pointer::authority = mint,
extensions::metadata_pointer::metadata_address = mint,
)]
pub mint: Box<InterfaceAccount<'info, Mint>>,
完整代码如下:
use anchor_lang::prelude::*;
use anchor_spl::token_interface::{token_metadata_initialize, Mint, TokenMetadataInitialize};
use anchor_spl::{
associated_token::AssociatedToken,
token_interface::Token2022,
token_2022::spl_token_2022::{
self,
},
token_2022::{spl_token_2022::instruction::AuthorityType, set_authority, SetAuthority}
};
use spl_pod::solana_program::program::invoke;
use spl_pod::solana_program::system_instruction::transfer;
use crate::constants::MINT_SEEDS;
use crate::errors::ErrorCode;
use crate::states::TokenMetadata;
#[derive(Accounts)]
#[instruction(metadata_params: TokenMetadata)]
pub struct InitializeToken<'info> {
#[account(
mut,
)]
pub payer: Signer<'info>,
/// CHECK: Defined the mint account by Anchor framework.
#[account(
init,
payer = payer,
seeds = [MINT_SEEDS.as_bytes(), metadata_params.name.as_bytes(), metadata_params.symbol.as_bytes()],
bump,
mint::token_program = token_program,
mint::decimals = 0,
mint::authority = mint,
mint::freeze_authority = mint,
extensions::metadata_pointer::authority = mint,
extensions::metadata_pointer::metadata_address = mint,
)]
pub mint: Box<InterfaceAccount<'info, Mint>>,
pub rent: Sysvar<'info, Rent>,
pub system_program: Program<'info, System>,
pub token_program: Program<'info, Token2022>,
pub associated_token_program: Program<'info, AssociatedToken>,
}
impl<'info> InitializeToken<'info> {
pub fn update_account_lamports_to_minimum_balance<'a>(
account: AccountInfo<'a>,
payer: AccountInfo<'a>,
system_program: AccountInfo<'a>,
) -> Result<()> {
let minimum_balance = Rent::get()?.minimum_balance(account.data_len());
let current_balance = account.get_lamports();
if minimum_balance > current_balance {
let extra_lamports = minimum_balance - current_balance;
invoke(
&transfer(payer.key, account.key, extra_lamports),
&[payer, account, system_program],
)?;
}
Ok(())
}
pub fn initialize_token(ctx: Context<InitializeToken>, metadata_params: Box<TokenMetadata>) -> Result<()> {
require!(spl_token_2022::ID == ctx.accounts.token_program.key(), ErrorCode::WrongTokenProgram);
// Create metadata account(inside the mint account)
let mint_account = ctx.accounts.mint.to_account_info();
let cpi_accounts = TokenMetadataInitialize {
token_program_id: ctx.accounts.token_program.to_account_info(),
mint: mint_account.to_account_info(),
metadata: mint_account.to_account_info(), // metadata account is the mint, since data is stored in mint
mint_authority: mint_account.to_account_info(),
update_authority: mint_account.to_account_info(),
};
let cpi_ctx = CpiContext::new(ctx.accounts.token_program.to_account_info(), cpi_accounts);
let token_name = metadata_params.clone().name;
let token_symbol = metadata_params.clone().symbol;
token_metadata_initialize(cpi_ctx, token_name.clone(), token_symbol.clone(), metadata_params.uri)?;
ctx.accounts.mint.reload()?;
Self::update_account_lamports_to_minimum_balance(
mint_account.to_account_info(),
ctx.accounts.payer.to_account_info(),
ctx.accounts.system_program.to_account_info(),
)?;
// Revoke the freeze authority
let seeds = &[MINT_SEEDS.as_bytes(), token_name.as_bytes(), token_symbol.as_bytes(), &[ctx.bumps.mint]];
let signer = [&seeds[..]];
set_authority(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
SetAuthority {
current_authority: ctx.accounts.mint.to_account_info(),
account_or_mint: ctx.accounts.mint.to_account_info(),
},
&signer,
),
AuthorityType::FreezeAccount,
None,
)?;
Ok(())
}
}
方法二
但是遇到复杂的应用场景,通过Anchor框架无法实现。比如,要创建一个同时加载Transfer fee和Metadata的mint,就无法实现。这是需要手动创建Mint。
手动创建带有metadata的Mint账户分为以下几步:
- 1- 创建Metadata Pointer Extension并分配存储空间
- 2- 初始化metadata pointer到mint账户自己
- 3- 创建mint账户
- 4- 在Extension空间内写入metadata数据
完整代码:
use anchor_lang::prelude::*;
use anchor_lang::solana_program::program::invoke_signed;
use anchor_lang::system_program::{create_account, CreateAccount};
use anchor_spl::{
associated_token::AssociatedToken,
token_interface::{
Token2022,
spl_pod::optional_keys::OptionalNonZeroPubkey
},
token_2022::spl_token_2022::{
self,
extension::{
ExtensionType,
metadata_pointer::instruction::initialize as initialize_metadata_pointer,
},
instruction::initialize_mint2,
pod::PodMint
},
token_2022::{spl_token_2022::instruction::AuthorityType, set_authority, SetAuthority}
};
use spl_token_metadata_interface::{
state::TokenMetadata as SplTokenMetadata,
instruction::initialize as initialize_metadata,
};
use crate::constants::MINT_SEEDS;
use crate::errors::ErrorCode;
use crate::states::TokenMetadata;
#[derive(Accounts)]
#[instruction(metadata_params: TokenMetadata)]
pub struct InitializeToken2<'info> {
#[account(
mut,
)]
pub payer: Signer<'info>,
/// CHECK: Defined the mint account manually instead of Anchor framework. Don't use `pub mint: Account<'info, Mint>`.
#[account(
mut,
seeds = [MINT_SEEDS.as_bytes(), metadata_params.name.as_bytes(), metadata_params.symbol.as_bytes()],
bump,
)]
pub mint: AccountInfo<'info>,
pub rent: Sysvar<'info, Rent>,
pub system_program: Program<'info, System>,
pub token_program: Program<'info, Token2022>,
pub associated_token_program: Program<'info, AssociatedToken>,
}
impl<'info> InitializeToken2<'info> {
pub fn initialize_token2(ctx: Context<InitializeToken2>, metadata_params: Box<TokenMetadata>) -> Result<()> {
require!(spl_token_2022::ID == ctx.accounts.token_program.key(), ErrorCode::WrongTokenProgram);
let seeds = &[MINT_SEEDS.as_bytes(), metadata_params.name.as_bytes(), metadata_params.symbol.as_bytes(), &[ctx.bumps.mint]];
let signer = [&seeds[..]];
// Create metadata point and allocate space
let mint_size = ExtensionType::try_calculate_account_len::<PodMint>(&[
ExtensionType::MetadataPointer
])?;
let spl_token_metadata = SplTokenMetadata {
update_authority: OptionalNonZeroPubkey(ctx.accounts.mint.key()),
mint: ctx.accounts.mint.key(),
name: metadata_params.name.clone(),
symbol: metadata_params.symbol.clone(),
uri: metadata_params.uri.clone(),
additional_metadata: vec![],
};
let metadata_size = SplTokenMetadata::tlv_size_of(&spl_token_metadata)?;
let lamports = (Rent::get()?).minimum_balance(mint_size + metadata_size);
create_account(
CpiContext::new_with_signer(
ctx.accounts.system_program.to_account_info(),
CreateAccount {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.mint.to_account_info(),
},
&signer,
),
lamports,
mint_size as u64,
&ctx.accounts.token_program.key(),
)?;
// Initialize metadata point
// API: https://github.com/solana-program/token-2022/blob/6ae924718a5a4b0a35a25795f6d581d8310559a4/program/src/extension/metadata_pointer/processor.rs#L27
// Same as anchor framework:
// extensions::metadata_pointer::authority = mint,
// extensions::metadata_pointer::metadata_address = mint,
invoke_signed(
&initialize_metadata_pointer(
&ctx.accounts.token_program.key(),
&ctx.accounts.mint.key(),
Some(ctx.accounts.mint.key()), // authority = mint
Some(ctx.accounts.mint.key()), // metadata address = mint
).unwrap(),
&[
ctx.accounts.mint.to_account_info(),
],
&signer,
)?;
// Initialize Mint Account
invoke_signed(
&initialize_mint2(
&ctx.accounts.token_program.key(),
&ctx.accounts.mint.key(),
&ctx.accounts.mint.key(),
Some(&ctx.accounts.mint.key()),
9,
)?,
&[
ctx.accounts.mint.to_account_info(),
ctx.accounts.rent.to_account_info(),
],
&signer,
)?;
// Initialize token metadata
invoke_signed(
&initialize_metadata(
&ctx.accounts.token_program.key(),
&ctx.accounts.mint.key(),
&ctx.accounts.mint.key(),
&ctx.accounts.mint.key(),
&ctx.accounts.mint.key(),
metadata_params.name.clone(),
metadata_params.symbol.clone(),
metadata_params.uri.clone(),
),
&[
ctx.accounts.mint.to_account_info(),
ctx.accounts.rent.to_account_info(),
],
&signer,
)?;
// Revoke the freeze authority
set_authority(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
SetAuthority {
current_authority: ctx.accounts.mint.to_account_info(),
account_or_mint: ctx.accounts.mint.to_account_info(),
},
&signer,
),
AuthorityType::FreezeAccount,
None,
)?;
Ok(())
}
}
在lib.rs中调用上面的指令
因为有两种方法,所以定义了两个调用指令:
pub fn initialize_token(ctx: Context<InitializeToken>, metadata_params: Box<TokenMetadata>) -> Result<()> {
InitializeToken::initialize_token(ctx, metadata_params)
}
pub fn initialize_token2(ctx: Context<InitializeToken2>, metadata_params: Box<TokenMetadata>) -> Result<()> {
InitializeToken2::initialize_token2(ctx, metadata_params)
}
编译部署
anchor build && anchor deploy
编写测试代码
编写两个测试代码,分别是simple version
,对应上面的initialize_token
指令;advanced version
,对应上面的initialize_token2
指令。
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Token2022 } from "../target/types/token_2022";
import { TOKEN_2022_PROGRAM_ID } from "@solana/spl-token";
import testAccounts from './accounts.json';
describe("token2022", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.Token2022 as Program<Token2022>;
const provider = anchor.AnchorProvider.env();
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));
const symbol = "STT" + Math.floor(Math.random() * 100);
const metadataParams = {
name: "Solana test token",
symbol,
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 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}`);
}
}
}
it("airdrop", async () => {
await airdropSol(5);
})
it("Initiazlia token (simple version)!", async () => {
const context = {
payer: deployerAccount.publicKey,
mint: mintAccount,
tokenProgram: TOKEN_2022_PROGRAM_ID,
}
const tx = await program.methods
.initializeToken(metadataParams)
.accounts(context)
.signers([deployerAccount])
.rpc();
console.log("Your transaction signature", tx);
})
it.skip("Initiazlia token (advanced version)!", async () => {
const context = {
payer: deployerAccount.publicKey,
mint: mintAccount,
tokenProgram: TOKEN_2022_PROGRAM_ID,
}
const tx = await program.methods
.initializeToken2(metadataParams)
.accounts(context)
.signers([deployerAccount])
.rpc();
console.log("Your transaction signature", tx);
})
})
运行后,两种方法分别创建了一个token,可以在Solana Explorer上查看:
1- 通过简易方法创建的Token,指令如下图:
2- 通过手动方法创建的Token,指令如下图:
无论那种方法,创建的token的扩展都有两个:Metadata Pointer Extension
, Token Metadata Extension
: