Token2022 - 使用Transfer fee扩展
本文在之前创建的包含Metadata Pointer Extension
和Token Metadata Extension
的代币的基础上,再添加一个扩展:Transfer Fee Extension
。
因为Anchor框架尚未开发这个扩展,所以需要手动创建这个扩展,然后在指令中添加这个扩展,最后在指令中调用这个扩展,就可以实现Transfer fee
的功能了。
创建指令
在instructions
目录下创建一个新的指令: initialize_token_with_transfer_fee.rs
,并将以下代码粘贴进去:
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, create, Create},
token_interface::{Token2022, spl_pod::optional_keys::OptionalNonZeroPubkey},
token_2022::spl_token_2022::{
self,
instruction::AuthorityType,
extension::{
ExtensionType,
metadata_pointer::instruction::initialize as initialize_metadata_pointer,
transfer_fee::instruction::initialize_transfer_fee_config
},
instruction::initialize_mint2,
pod::PodMint
},
token_2022::{mint_to, MintTo, 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 InitializeTokenWithTransferFee<'info> {
#[account(mut)]
pub payer: Signer<'info>,
/// CHECK: token account of payer
#[account(mut)]
pub payer_ata: UncheckedAccount<'info>,
/// CHECK: token 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> InitializeTokenWithTransferFee<'info> {
pub fn initialize_token_with_transfer_fee(ctx: Context<InitializeTokenWithTransferFee>, metadata_params: Box<TokenMetadata>, transfer_fee_basis_points: u16, maximum_fee: u64) -> 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 account and allocate space
let mint_size = ExtensionType::try_calculate_account_len::<PodMint>(&[
ExtensionType::TransferFeeConfig,
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 transfer fee config
invoke_signed(
&initialize_transfer_fee_config(
&ctx.accounts.token_program.key(), // SPL Token program ID
&ctx.accounts.mint.key(), // Mint account
Some(&ctx.accounts.mint.key()),
Some(&ctx.accounts.mint.key()),
transfer_fee_basis_points, // Basis points for the transfer fee
maximum_fee,// Maximum fee
).unwrap(),
&[
ctx.accounts.mint.to_account_info().clone(),
],
&signer,
)?;
// Initialize metadata point
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().clone(),
],
&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().clone(),
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().clone(),
ctx.accounts.mint.to_account_info().clone(),
],
&signer,
)?;
// Create payer's ATA if not exists
if ctx.accounts.payer_ata.to_account_info().data_is_empty() {
create(
CpiContext::new(
ctx.accounts.associated_token_program.to_account_info(),
Create {
payer: ctx.accounts.payer.to_account_info(),
associated_token: ctx.accounts.payer_ata.to_account_info(),
authority: ctx.accounts.payer.to_account_info(),
mint: ctx.accounts.mint.to_account_info(),
system_program: ctx.accounts.system_program.to_account_info(),
token_program: ctx.accounts.token_program.to_account_info(),
})
)?;
}
// Mint 10000000 to deployer
mint_to(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
MintTo {
mint: ctx.accounts.mint.to_account_info(),
to: ctx.accounts.payer_ata.to_account_info(),
authority: ctx.accounts.mint.to_account_info(),
},
&signer,
),
10000000 * 10u64.pow(metadata_params.decimals as u32),
)?;
// 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().clone(),
account_or_mint: ctx.accounts.mint.to_account_info().clone(),
},
&signer,
),
AuthorityType::FreezeAccount,
None,
)?;
// Revoke the mint authority
set_authority(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
SetAuthority {
current_authority: ctx.accounts.mint.to_account_info().clone(),
account_or_mint: ctx.accounts.mint.to_account_info().clone(),
},
&signer,
),
AuthorityType::MintTokens,
None,
)?;
Ok(())
}
}
以上指令创建了一个新的Mint,按顺序执行了一下操作:
- 1- 计算一个包含
Transfer fee
扩展和Metadata Pointer
扩展的mint需要的空间,以及需要的lamports,并创建账户; - 2- 初始化
Transfer fee config
账户,并设置Transfer fee
费率和最大费用; - 3- 初始化
Metadata Pointer
账户,并设置Metadata Pointer
账户的authority
为mint
账户,metadata address
为mint
账户; - 4- 使用
initialize_mint2
指令创建mint账户; - 5- 使用
initialize_metadata
指令创建metadata
; - 6- 创建部署人的TokenAccount;
- 7- 向部署人的token account账户铸造10000000个代币,用于之后的转账测试;
- 8- 取消freeze代币的权限;
- 9- 取消mint代币的权限;
在lib.rs中调用上面的指令
pub fn initialize_token_with_transfer_fee(ctx: Context<InitializeTokenWithTransferFee>, metadata_params: Box<TokenMetadata>, transfer_fee_basis_points: u16, maximum_fee: u64) -> Result<()> {
InitializeTokenWithTransferFee::initialize_token_with_transfer_fee(ctx, metadata_params, transfer_fee_basis_points, maximum_fee)
}
编译部署
anchor build && anchor deploy
编写测试代码
describe("Test Transfer fee extension", async () => {
it("Create token mint with transfer fee extension", async () => {
const payerAta = getAssociatedTokenAddressSync(mintAccount, deployerAccount.publicKey, false, TOKEN_2022_PROGRAM_ID);
const context = {
payer: deployerAccount.publicKey,
mint: mintAccount,
payerAta: payerAta,
tokenProgram: TOKEN_2022_PROGRAM_ID,
}
const tranferFeeBasisPoints = 100;
const maximumFee = new anchor.BN(100000).mul(new anchor.BN(1000000000));
const tx = new anchor.web3.Transaction();
const ix = await program.methods
.initializeTokenWithTransferFee(metadataParams, tranferFeeBasisPoints, maximumFee)
.accounts(context)
.instruction();
tx.add(ix);
const signature = await provider.sendAndConfirm(tx, [deployerAccount]);
console.log("Initialized token with transfer fee extension tx:", signature);
})
it("Transfer token with transfer fee extension (from deployer to user1)", async () => {
// Get the source and destination token accounts
const sourceAta = await getAssociatedTokenAddress(mintAccount, deployerAccount.publicKey, false, TOKEN_2022_PROGRAM_ID);
const destinationAta = await getAssociatedTokenAddress(mintAccount, user1Account.publicKey, false, TOKEN_2022_PROGRAM_ID);
// Create destination ATA if it doesn't exist
const tx = new anchor.web3.Transaction();
try {
await provider.connection.getTokenAccountBalance(destinationAta);
} catch {
// if token accont the destination doesn't exist, create it
const ix = createAssociatedTokenAccountInstruction(
deployerAccount.publicKey,
destinationAta,
user1Account.publicKey,
mintAccount,
TOKEN_2022_PROGRAM_ID,
);
tx.add(ix);
// await provider.sendAndConfirm(tx, [user1Account]);
console.log("Created destination ATA:", destinationAta.toBase58());
}
// Transfer amount: 100 tokens (considering 9 decimals)
const amount = new anchor.BN(1000).mul(new anchor.BN(1000000000)); // 1000 tokens
const fee = amount.mul(new anchor.BN(100)).div(new anchor.BN(10000));
const transferIx = createTransferCheckedWithFeeInstruction(
sourceAta,
mintAccount,
destinationAta,
deployerAccount.publicKey,
BigInt(amount.toString()),
9,
BigInt(fee.toString()),
[],
TOKEN_2022_PROGRAM_ID,
)
// Send transaction
tx.add(transferIx);
const signature = await provider.sendAndConfirm(tx, [deployerAccount]);
console.log("Transfer transaction signature:", signature);
// Log balances
const sourceBalance = await provider.connection.getTokenAccountBalance(sourceAta);
const destBalance = await provider.connection.getTokenAccountBalance(destinationAta);
console.log("Source balance after transfer:", sourceBalance.value.uiAmount);
console.log("Destination balance after transfer:", destBalance.value.uiAmount);
})
})
上面执行了两个测试程序:
-
1- 部署一个新的Token,执行后,将交易hash复制到区块链浏览器,可以看到以下交易指令的记录:
-
2- 执行转账操作 注意当时用
Transfer fee extension
转账时,需要使用createTransferCheckedWithFeeInstruction()
方法创建指令。
执行后,交易记录见下图:
转账了1000个代币,费用为1%,有10个代币进到了收款人的token account中,实际收到990个。
进到收款人的token account中,可以通过Solana Explorer浏览器查看到: