Jacky Gu

Solana开发笔记: Token2022 - 创建Mint代币工厂

01 Jan 2025 Share to

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: