LogoAnchor 中文文档

创建Token账户

学习如何使用Anchor在Solana程序中创建和初始化Token账户。涵盖创建关联Token账户(ATA)和程序派生地址(PDA)Token账户的代码示例。

什么是Token账户?

Token账户是Solana Token Programs中的一种账户类型,用于存储个人对特定Token(mint)的所有权信息。每个Token账户都与一个单独的mint相关联,并跟踪Token余额和所有者等详细信息。

/// Account data.
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Account {
    /// The mint associated with this account
    pub mint: Pubkey,
    /// The owner of this account.
    pub owner: Pubkey,
    /// The amount of tokens this account holds.
    pub amount: u64,
    /// If `delegate` is `Some` then `delegated_amount` represents
    /// the amount authorized by the delegate
    pub delegate: COption<Pubkey>,
    /// The account's state
    pub state: AccountState,
    /// If `is_native.is_some`, this is a native token, and the value logs the
    /// rent-exempt reserve. An Account is required to be rent-exempt, so
    /// the value is used by the Processor to ensure that wrapped SOL
    /// accounts do not drop below this threshold.
    pub is_native: COption<u64>,
    /// The amount delegated
    pub delegated_amount: u64,
    /// Optional authority to close the account.
    pub close_authority: COption<Pubkey>,
}

请注意,在源代码中,Token账户被称为Account类型。Token ProgramToken Extension Program都有相同的Token账户基本实现。

为了持有特定mint的Token,用户必须首先创建一个Token账户。每个Token账户都与以下内容相关联:

  1. 一个特定的mint(Token账户持有的Token类型)
  2. 一个所有者(可以从账户中转出Token的权限)

我们以Solana上的USDC为例:

  • USDC的mint地址是EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
  • Circle(USDC的发行方)在3emsAVdmGKERbHjmGfQ6oZ1e35dkf5iYcS6U4CPKFVaa有一个Token账户
  • 这个Token账户只能持有USDC Token(mint)
  • Circle被设置为该账户的所有者,地址为7VHUFJHWu2CuExkJcJrzhQPJ2oygupTWkL2A2For4BmE,并且可以转移这些Token

你可以在Solana Explorer上查看该Token账户的详细信息。

术语“owner”在两种不同的上下文中使用:

  1. Token账户的“owner”——这是存储在Token账户“owner”字段中的地址,由Token Program定义的Account类型指定。所有者可以转移、销毁或委托账户中的Token。该地址有时被称为Token账户的“authority”,以区别于程序的owner。

  2. 程序的“owner”——这指的是拥有Solana上账户数据的程序。对于Token账户,这始终是Token Program或Token Extension Program,如Solana基础Account类型中的“owner”字段所指定。

在处理Token账户时,“owner”通常指的是可以花费Token的权限,而不是拥有该账户的程序。

什么是关联Token账户?

关联Token账户(ATA)简单来说就是通过关联Token Program派生和创建的Token账户,其地址是一个PDA。你可以将ATA视为用户持有特定Token(mint)单元的默认Token账户。

只有通过关联Token Program创建的Token账户才被称为关联Token账户。

ATAs提供了一种确定性的方式来查找用户对任何给定mint的Token账户。你可以在这里查看派生实现。

关联Token账户地址派生
pub fn get_associated_token_address_and_bump_seed_internal(
    wallet_address: &Pubkey,
    token_mint_address: &Pubkey,
    program_id: &Pubkey,
    token_program_id: &Pubkey,
) -> (Pubkey, u8) {
    Pubkey::find_program_address(
        &[
            &wallet_address.to_bytes(), // 所有者的公钥
            &token_program_id.to_bytes(), // Token Program或Token Extension Program
            &token_mint_address.to_bytes(), // Token mint地址
        ],
        program_id, // 关联Token Program ID
    )
}

这种确定性派生确保对于任何钱包地址和Token mint的组合,只有一个关联Token账户地址存在。这种方法使得查找用户对任何给定Token mint的Token账户变得简单,无需单独跟踪Token账户地址。

关联Token Program作为帮助程序,创建具有确定性地址(PDA)的Token账户。在创建关联Token账户时,关联Token Program会向Token Program或Token Extension Program发出CPI(跨程序调用)。创建的账户由Token Program拥有,并具有与Token Program中定义的相同的Account类型结构。关联Token Program本身不维护状态——它只是提供了一种标准化的方法来创建具有确定性地址的Token账户。

使用说明

使用anchor-spl crate中的token_interfaceassociated_token模块来操作与Token Program和Token Extension Program兼容的ATAs。

代码片段

use anchor_spl::associated_token::AssociatedToken;
use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface};
 
// --snip--
 
#[derive(Accounts)]
pub struct CreateTokenAccount<'info> {
    #[account(mut)]
    pub signer: Signer<'info>,
    #[account(
        init_if_needed,
        payer = signer,
        associated_token::mint = mint,
        associated_token::authority = signer,
        associated_token::token_program = token_program,
    )]

    pub token_account: InterfaceAccount<'info, TokenAccount>,
    pub mint: InterfaceAccount<'info, Mint>,
    pub token_program: Interface<'info, TokenInterface>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub system_program: Program<'info, System>,
}

要使用从你的程序派生的PDA创建Token账户,你可以使用token::minttoken::authoritytoken::token_program约束以及seedsbump约束。

代码片段
use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface};
 
// --snip--
 
#[derive(Accounts)]
pub struct CreateTokenAccount<'info> {
    #[account(mut)]
    pub signer: Signer<'info>,
    #[account(



        init_if_neeeded,
        payer = signer,
        token::mint = mint,
        token::authority = token_account,
        token::token_program = token_program,
        seeds = [b"token"],
        bump
    )]
    pub token_account: InterfaceAccount<'info, TokenAccount>,
    pub mint: InterfaceAccount<'info, Mint>,
    pub token_program: Interface<'info, TokenInterface>,
    pub system_program: Program<'info, System>,
}

账户类型

InterfaceAccount类型是一个包装器,允许账户与Token Program和Token Extension Program一起工作。

TokenAccount类型表示两种Token程序共享的基本Account数据结构。当传递此类型的账户时,Anchor会自动将账户数据反序列化为Account结构体,而不管是由哪个Token程序创建的。

账户类型
pub token_account: InterfaceAccount<'info, TokenAccount>,

账户约束

Anchor提供了两组用于处理Token账户的约束:

  • 当使用关联Token账户(ATA)时,使用associated_token约束
  • 当使用非特定ATA的Token账户(如自定义PDA或使用密钥对公钥作为地址的Token账户)时,使用token约束

使用哪种约束取决于你的具体用例。对于用户钱包,推荐使用ATAs,而对于程序控制的账户,自定义Token账户则更为有用。

associated_token约束

以下账户约束组合用于创建和初始化新的关联Token账户:

约束描述
init通过向System Program发出跨程序调用(CPI)来创建新账户。这将为Token账户分配所需的空间,并将所有权转移给适当的Token程序。
init_if_needed类似于init,但仅在账户不存在时创建。需要启用init-if-needed功能。
payer指定将支付创建新账户所需租金(SOL存款)的账户。
associated_token::mint指定与此Token账户关联的mint账户。
associated_token::authority设置Token账户的权限(owner),有权转移或销毁Token。
associated_token::token_program指定创建Token账户时使用的Token程序(Token Program或Token Extension Program)。
创建关联Token账户
#[account(
    init,
    payer = <payer>,
    associated_token::mint = <mint>,
    associated_token::authority = <authority>,
    associated_token::token_program = <token_program>
)]
pub token_account: InterfaceAccount<'info, TokenAccount>,

token约束

以下账户约束组合用于创建和初始化新的Token账户:

约束描述
--------
init通过向System Program发出跨程序调用(CPI)来创建新账户。这将为Token账户分配所需的空间,并将所有权转移给适当的Token程序。
init_if_needed类似于init,但仅在账户不存在时创建。需要启用init-if-needed功能。
payer指定将支付创建新账户所需租金(SOL存款)的账户。
token::mint指定与此Token账户关联的mint账户。
token::authority设置Token账户的权限(owner),有权转移或销毁Token。
token::token_program指定创建Token账户时使用的Token程序(Token Program或Token Extension Program)。
使用密钥对公钥作为地址创建Token账户
#[account(
    init,
    payer = <payer>,
    token::mint = <mint>,
    token::authority = <authority>,
    token::token_program = <token_program>
)]
pub token_account: InterfaceAccount<'info, TokenAccount>,
使用PDA作为地址创建Token账户
#[account(
    init,
    payer = <payer>,
    token::mint = <mint>,
    token::authority = <authority>,
    token::token_program = <token_program>,
    seeds = [<seeds>],
    bump
)]
pub token_account: InterfaceAccount<'info, TokenAccount>,

请注意,你可以将同一个PDA用作token::authority和Token账户地址。使用PDA作为token::authority允许你的程序“签署”CPI指令以从Token账户转移Token。这种模式允许为这两种用途使用单个确定性地址。

要使用init_if_needed约束,请在Cargo.toml中启用init-if-needed功能,并将init约束替换为init_if_needed

Cargo.toml
[dependencies]
anchor-lang = { version = "0.30.1", features = ["init-if-needed"] }

示例

以下示例展示了在Anchor程序中使用两种不同方法创建Token账户:

  1. 使用关联Token账户(ATA)——这是为特定用户创建持有特定Token(mint)单元的标准方法。

  2. 使用程序派生地址(PDA)——这种方法创建的Token账户地址是一个自定义的PDA。这允许为你的程序创建确定性的Token账户地址。你还可以将权限(owner)设置为PDA,以便你的程序可以从Token账户转移Token。

这两种方法都可以完全使用账户约束来实现。

创建关联Token账户

On this page

在GitHub上编辑