SPL Token是Solana针对ERC20的一种实现,在Solana编程中常常会对SPL Token进行操作,今天介绍一些关于SPL Token的最佳实践,代码出自Solana官方之手:spl_tokes.rs
/// 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的状态,校验是否初始化
/// 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的状态,校验是否初始化
/// 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。
/// 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其实是不同的。
/// 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))
}
代码和上面类似
/// 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字段里,需要自己解析。
/// 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是否是签名者。
/// 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。
/// 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方法执行指令。
/// 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。
/// 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字段。