当前位置: 首页 > 工具软件 > Solana > 使用案例 >

Solana SPL Token 的一些最佳实践

乜昆
2023-12-01

SPL Token是Solana针对ERC20的一种实现,在Solana编程中常常会对SPL Token进行操作,今天介绍一些关于SPL Token的最佳实践,代码出自Solana官方之手:spl_tokes.rs

1、Token Account 校验

/// Asserts the given account_info represents a valid SPL Token account which is initialized and belongs to spl_token program
pub fn assert_is_valid_spl_token_account(account_info: &AccountInfo) -> Result<(), ProgramError> {
    if account_info.data_is_empty() {
        return Err(GovernanceError::SplTokenAccountDoesNotExist.into());
    }

    if account_info.owner != &spl_token::id() {
        return Err(GovernanceError::SplTokenAccountWithInvalidOwner.into());
    }

    if account_info.data_len() != Account::LEN {
        return Err(GovernanceError::SplTokenInvalidTokenAccountData.into());
    }

    // TokeAccount layout:   mint(32), owner(32), amount(8), delegate(36), state(1), ...
    let data = account_info.try_borrow_data()?;
    let state = array_ref![data, 108, 1];

    if state == &[0] {
        return Err(GovernanceError::SplTokenAccountNotInitialized.into());
    }

    Ok(())
}

Token Account是SPL Token里特有的Account类型,它的作用是用来持有某种类型的代币,每个用户想要持有代币,就必须创建一个Token Account。在转移代币的时候,我们往往需要校验一个Token Account是合法的,上述代码包含了所有需要校验的点:

1. 校验data是否为空,如果为空说明account有问题

2. 校验account的owner是否是SPL Token程序,所有用户创建的Token Account的owner都必须是SPL Token程序

3. 校验data的长度是否是合法的,这里的Account数据结构是SPL Token程序里定义的

4. 获取Token Account的状态,校验是否初始化

2、Token Mint 校验

/// Asserts the given mint_info represents a valid SPL Token Mint account  which is initialized and belongs to spl_token program
pub fn assert_is_valid_spl_token_mint(mint_info: &AccountInfo) -> Result<(), ProgramError> {
    if mint_info.data_is_empty() {
        return Err(GovernanceError::SplTokenMintDoesNotExist.into());
    }

    if mint_info.owner != &spl_token::id() {
        return Err(GovernanceError::SplTokenMintWithInvalidOwner.into());
    }

    if mint_info.data_len() != Mint::LEN {
        return Err(GovernanceError::SplTokenInvalidMintAccountData.into());
    }

    // In token program [36, 8, 1, is_initialized(1), 36] is the layout
    let data = mint_info.try_borrow_data().unwrap();
    let is_initialized = array_ref![data, 45, 1];

    if is_initialized == &[0] {
        return Err(GovernanceError::SplTokenMintNotInitialized.into());
    }

    Ok(())
}

每个Token Account都会关联一个Mint,这个Mint保存了币的信息,你可以把一个Mint账户当成一个币,而每个和这个Mint关联的Token Account都是可以持有这个币的一个钱包。在做转账的时候,除了要校验Token Account,最好也校验一下Mint:

1. 校验data是否为空,如果为空说明account有问题

2. 校验account的owner是否是SPL Token程序,所有Mint Account的owner都必须是SPL Token程序

3. 校验data的长度是否是合法的,这里的Mint数据结构是SPL Token程序里定义的

4. 获取Mint Account的状态,校验是否初始化

3、获取Token Account的Mint信息

/// Computationally cheap method to get mint from a token account
/// It reads mint without deserializing full account data
pub fn get_spl_token_mint(token_account_info: &AccountInfo) -> Result<Pubkey, ProgramError> {
    assert_is_valid_spl_token_account(token_account_info)?;

    // TokeAccount layout:   mint(32), owner(32), amount(8), ...
    let data = token_account_info.try_borrow_data()?;
    let mint_data = array_ref![data, 0, 32];
    Ok(Pubkey::new_from_array(*mint_data))
}

Token Account只是一个数据结构,真正的内容存储在它的data字段里,这里通过try_borrow_data()获取所有的内容,然后拿到前32位的字节,这些内容就是Mint账户的public key。

4、获取Token Account的owner信息

/// Computationally cheap method to get owner from a token account
/// It reads owner without deserializing full account data
pub fn get_spl_token_owner(token_account_info: &AccountInfo) -> Result<Pubkey, ProgramError> {
    assert_is_valid_spl_token_account(token_account_info)?;

    // TokeAccount layout:   mint(32), owner(32), amount(8)
    let data = token_account_info.try_borrow_data()?;
    let owner_data = array_ref![data, 32, 32];
    Ok(Pubkey::new_from_array(*owner_data))
}

获取过程和上面的类型,值得注意的是,这里的owner_data和上面做校验时候的owner是两个概念。上面的owner指的是Token Account属于哪个程序,所有的Token Account的owner其实都是SPL Token程序,而这里的owner_data是从data里解析出来的,是用户自己定义的,这里的owner_data其实是指这些币的归属人,不同的Token Account里的owner_data其实是不同的。

5、获取币的最大供应量

/// Computationally cheap method to just get supply from a mint without unpacking the whole object
pub fn get_spl_token_mint_supply(mint_info: &AccountInfo) -> Result<u64, ProgramError> {
    assert_is_valid_spl_token_mint(mint_info)?;
    // In token program, 36, 8, 1, 1 is the layout, where the first 8 is supply u64.
    // so we start at 36.
    let data = mint_info.try_borrow_data().unwrap();
    let bytes = array_ref![data, 36, 8];

    Ok(u64::from_le_bytes(*bytes))
}

代码和上面类似

6、获取Mint Account的mint权限

/// Computationally cheap method to just get authority from a mint without unpacking the whole object
pub fn get_spl_token_mint_authority(
    mint_info: &AccountInfo,
) -> Result<COption<Pubkey>, ProgramError> {
    assert_is_valid_spl_token_mint(mint_info)?;
    // In token program, 36, 8, 1, 1 is the layout, where the first 36 is authority.
    let data = mint_info.try_borrow_data().unwrap();
    let bytes = array_ref![data, 0, 36];

    unpack_coption_pubkey(bytes)
}

代码和上面类型,mint权限就是指那个账号可以mint出新的币,这个权限存在Mint Account的data字段里,需要自己解析。

7、校验签名者有mint权限

/// Asserts current mint authority matches the given authority and it's signer of the transaction
pub fn assert_spl_token_mint_authority_is_signer(
    mint_info: &AccountInfo,
    mint_authority_info: &AccountInfo,
) -> Result<(), ProgramError> {
    let mint_authority = get_spl_token_mint_authority(mint_info)?;

    if mint_authority.is_none() {
        return Err(GovernanceError::MintHasNoAuthority.into());
    }

    if !mint_authority.contains(mint_authority_info.key) {
        return Err(GovernanceError::InvalidMintAuthority.into());
    }

    if !mint_authority_info.is_signer {
        return Err(GovernanceError::MintAuthorityMustSign.into());
    }

    Ok(())
}

先拿到Mint Account里的mint权限账号,如果没有设置就返回错误,如果传入的mint_authority_info不属于mint权限列表,就返回错误,最后判断一下mint_authority_info是否是签名者。

8、判断Token Account的token_owner是不是签名者

/// Asserts current token owner matches the given owner and it's signer of the transaction
pub fn assert_spl_token_owner_is_signer(
    token_info: &AccountInfo,
    token_owner_info: &AccountInfo,
) -> Result<(), ProgramError> {
    let token_owner = get_spl_token_owner(token_info)?;

    if token_owner != *token_owner_info.key {
        return Err(GovernanceError::InvalidTokenOwner.into());
    }

    if !token_owner_info.is_signer {
        return Err(GovernanceError::TokenOwnerMustSign.into());
    }

    Ok(())
}

 代码和上面类型,区别是解析的Token Account的data。

9、个人转账

/// Transfers SPL Tokens
pub fn transfer_spl_tokens<'a>(
    source_info: &AccountInfo<'a>,
    destination_info: &AccountInfo<'a>,
    authority_info: &AccountInfo<'a>,
    amount: u64,
    spl_token_info: &AccountInfo<'a>,
) -> ProgramResult {
    let transfer_instruction = spl_token::instruction::transfer(
        &spl_token::id(),
        source_info.key,
        destination_info.key,
        authority_info.key,
        &[],
        amount,
    )
    .unwrap();

    invoke(
        &transfer_instruction,
        &[
            spl_token_info.clone(),
            authority_info.clone(),
            source_info.clone(),
            destination_info.clone(),
        ],
    )?;

    Ok(())
}

个人对个人转账,首先生成一个转账的指令,指令需要包含SPL Token程序ID,源账户,目标账户,源账户的转账权限,转账数量。最后通过invoke方法执行指令。

10、PDA账户给个人账户转账

/// Transfers SPL Tokens from a token account owned by the provided PDA authority with seeds
pub fn transfer_spl_tokens_signed<'a>(
    source_info: &AccountInfo<'a>,
    destination_info: &AccountInfo<'a>,
    authority_info: &AccountInfo<'a>,
    authority_seeds: &[&[u8]],
    program_id: &Pubkey,
    amount: u64,
    spl_token_info: &AccountInfo<'a>,
) -> ProgramResult {
    let (authority_address, bump_seed) = Pubkey::find_program_address(authority_seeds, program_id);

    if authority_address != *authority_info.key {
        msg!(
                "Transfer SPL Token with Authority PDA: {:?} was requested while PDA: {:?} was expected",
                authority_info.key,
                authority_address
            );
        return Err(ProgramError::InvalidSeeds);
    }

    let transfer_instruction = spl_token::instruction::transfer(
        &spl_token::id(),
        source_info.key,
        destination_info.key,
        authority_info.key,
        &[],
        amount,
    )
    .unwrap();

    let mut signers_seeds = authority_seeds.to_vec();
    let bump = &[bump_seed];
    signers_seeds.push(bump);

    invoke_signed(
        &transfer_instruction,
        &[
            spl_token_info.clone(),
            authority_info.clone(),
            source_info.clone(),
            destination_info.clone(),
        ],
        &[&signers_seeds[..]],
    )?;

    Ok(())
}

控制PDA账户的权限账户往往是通过seeds生成的,所以在执行指令的时候需要提供seeds。首先通过find_program_address方法查出该PDA账户对应的权限账户,然后和传入的authority_info进行比较,防止用户传一个假的账户。然后和上面一样生成转账指令,最后通过invoke_seeds方法进行指令执行,这个方法往往用于对PDA账户的操作,执行的时候需要传入对应的seeds。

11、生成一个Token Account

/// Creates and initializes SPL token account with PDA using the provided PDA seeds
#[allow(clippy::too_many_arguments)]
pub fn create_spl_token_account_signed<'a>(
    payer_info: &AccountInfo<'a>,
    token_account_info: &AccountInfo<'a>,
    token_account_address_seeds: &[&[u8]],
    token_mint_info: &AccountInfo<'a>,
    token_account_owner_info: &AccountInfo<'a>,
    program_id: &Pubkey,
    system_info: &AccountInfo<'a>,
    spl_token_info: &AccountInfo<'a>,
    rent_sysvar_info: &AccountInfo<'a>,
    rent: &Rent,
) -> Result<(), ProgramError> {
    let create_account_instruction = system_instruction::create_account(
        payer_info.key,
        token_account_info.key,
        1.max(rent.minimum_balance(spl_token::state::Account::get_packed_len())),
        spl_token::state::Account::get_packed_len() as u64,
        &spl_token::id(),
    );

    let (account_address, bump_seed) =
        Pubkey::find_program_address(token_account_address_seeds, program_id);

    if account_address != *token_account_info.key {
        msg!(
            "Create SPL Token Account with PDA: {:?} was requested while PDA: {:?} was expected",
            token_account_info.key,
            account_address
        );
        return Err(ProgramError::InvalidSeeds);
    }

    let mut signers_seeds = token_account_address_seeds.to_vec();
    let bump = &[bump_seed];
    signers_seeds.push(bump);

    invoke_signed(
        &create_account_instruction,
        &[
            payer_info.clone(),
            token_account_info.clone(),
            system_info.clone(),
        ],
        &[&signers_seeds[..]],
    )?;

    let initialize_account_instruction = spl_token::instruction::initialize_account(
        &spl_token::id(),
        token_account_info.key,
        token_mint_info.key,
        token_account_owner_info.key,
    )?;

    invoke(
        &initialize_account_instruction,
        &[
            payer_info.clone(),
            token_account_info.clone(),
            token_account_owner_info.clone(),
            token_mint_info.clone(),
            spl_token_info.clone(),
            rent_sysvar_info.clone(),
        ],
    )?;

    Ok(())
}

首先生成一个创建账户的指令,这里的token_account_info是个PDA地址,所以需要校验一下seeds和地址是否一致。然后Solana系统程序会执行这个创建指令,此时Solana会为这个PDA地址分配空间生成一个AccountInfo结构体。然后生成一个初始化Token Account的指令,这个指令只需要调用invoke,此时是由SPL Token程序来执行的,它会往刚才创建的AccountInfo的data字段里写入Token Account 相关的信息。综上所述,我们可以知道,一个Token Account其实就是一个地址+AccountInfo+data字段存储的内容,不同业务程序其实处理的都是data字段。

 类似资料: