Solana中利用Anchor自动解析TokenAccount

Posted MateZero

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Solana中利用Anchor自动解析TokenAccount相关的知识,希望对你有一定的参考价值。

Solana中利用Anchor自动解析TokenAccount

一、背景介绍

在Solana区块链中,绝大多数应用都会涉及到spl-token,因此获取用户账号(TokenAccount)中的信息是一个很常见的需求(对应spl_token::state::Account结构),例如需要知道账号对应的代币(mint),或者账户余额,又或者拥有者等。而传统的方法是手动解析其底层数据,获取相应位置及长度的数据切片,然后再转换成对应的数据结构。这种手动转换不仅繁琐,需要记清每个数据结构的位置及长度,还容易出错。那么有没有一种新的方法能自动进行反序列化呢?(因为我们的合约不是spl-token合约,是无法写数据的,因此我们只需要进行反序列化解析数据就行了)

答案是有的,Solana上有一个开发工具叫Anchor,其工作方式类似以太坊区块链,使用类似abi的idl进行链上交易,并且可以自动进行数据序列化与反序列化,不需要自己写borrow_data之类的函数了。

其文档为https://project-serum.github.io/anchor/getting-started/introduction.html

Anchor另一个好处是客户端可以直接调用合约中对应的外部接口(函数),而不必所有调用统一走传统合约的entrypoint再匹配不同的Instruction进行不同的分支处理(这种方式显得有些low)。因此,Anchor工具大大节省了开发时间,提升了开发效率,强烈推荐大家使用。

二、示例代码

话不多说,我们直接上使用Anchor获取/解析TokenAccount数据的示例代码。这里的TokenAccount账号就是主账号在某个代币下对应的账号。

至于使用Anchor创建工程,编译和部署我们这里就不讲了,大家可以看其文档。

lib.rs

///本程序用来示例使用Anchor自动反序列化解析spl-token中的Account数据
use anchor_lang::prelude::*;
use anchor_spl::token::TokenAccount;
use spl_token::state::AccountState;
use solana_program::
    msg,
    program_option::COption,
;

// anchor build后使用solana address -k *.json 获取后替换
declare_id!("5euCBn8q5A3XSvpUkZGVbuwmsPdyLG9CaotEhaw6wHgX");

#[program]
mod token_account_info 
    use super::*;
    pub fn get_token_account_info(ctx: Context<TokenAccountInfo>) -> ProgramResult 
        //获取TokenAccountInfo中的my_account账号
        let my_account = &ctx.accounts.my_account; //底层类型为TokenAccount

        //获取账号本身的相关信息(不是在spl-token中的信息)
        let info = my_account.as_ref(); //类型为AccountInfo
        msg!("account_key:",info.key());
        msg!("account_owner:",info.owner); //这里owner应该为spl-token
        msg!("account_is_signer:",info.is_signer);
        msg!("account_is_writable:",info.is_writable);
        msg!("account_lamports:",info.lamports());
        msg!("account_data_len:",info.data_len());

        //获取spl-token中Account信息
        msg!("address:",my_account.key());
        msg!("balance:",my_account.amount);
        msg!("mint:",my_account.mint);
        msg!("owner:",my_account.owner);
        msg!("state:", match my_account.state 
            AccountState::Uninitialized => "Uninitialized",
            AccountState::Initialized => "Initialized",
            AccountState::Frozen => "Frozen"
        );
        if let COption::Some(key) = my_account.delegate 
            msg!("Delegation:",key);
         else 
            msg!("Delegation:None");
        
        if let COption::Some(key) = my_account.close_authority 
            msg!("Close authority:",key);
         else 
            msg!("Close authority:None");
        
        
        Ok(())
    


#[derive(Accounts)]
pub struct TokenAccountInfo<'info> 
    #[account()]
    my_account:Account<'info,TokenAccount>,  //这里会自动判定owner是否为spl-token


在这里,my_account的类型为一个Account<'info, T> ,相关的代码片断为:

///定义
#[derive(Clone)]
pub struct Account<'info, T: AccountSerialize + AccountDeserialize + Owner + Clone> 
    account: T,
    info: AccountInfo<'info>,


.....

///实现了 AsRef 用来获取其AccountInfo
impl<'info, T: AccountSerialize + AccountDeserialize + Owner + Clone> AsRef<AccountInfo<'info>>
    for Account<'info, T>

    fn as_ref(&self) -> &AccountInfo<'info> 
        &self.info
    


......

/// 在进行反序列化之前,检查了owner,这里的T::owner()为spl-token的program ID
#[inline(never)]
    pub fn try_from_unchecked(info: &AccountInfo<'a>) -> Result<Account<'a, T>, ProgramError> 
        if info.owner != &T::owner() 
            return Err(ErrorCode::AccountNotProgramOwned.into());
        
        let mut data: &[u8] = &info.try_borrow_data()?;
        Ok(Account::new(
            info.clone(),
            T::try_deserialize_unchecked(&mut data)?,
        ))
    

因此,TokenAccount 结构必须实现AccountSerialize + AccountDeserialize + Owner + Clone这几种特型。

三、客户端调用

客户端相应的调用为:

client.js

// client.js is used to introduce the reader to generating clients from IDLs.
const anchor = require('@project-serum/anchor');

// Configure the local cluster.
anchor.setProvider(anchor.Provider.local());

async function main() 
  // #region main
  // Read the generated IDL.
  const idl = JSON.parse(require('fs').readFileSync('./target/idl/token_account_info.json', 'utf8'));

  // Address of the deployed program.
  const programId = new anchor.web3.PublicKey('5euCBn8q5A3XSvpUkZGVbuwmsPdyLG9CaotEhaw6wHgX');

  // Generate the program client from IDL.
  const program = new anchor.Program(idl, programId);

  // Execute the RPC.
  await program.rpc.getTokenAccountInfo(
    accounts:
      myAccount:new anchor.web3.PublicKey("5jDKccZvVkf7sRM3GgjBxURdrGM7Q2z5EfoKHi9cYVhj")
    
  );
  // #endregion main


console.log('Running client.');
main().then(() => console.log('Success'));

四、anchor_spl::token 学习

我们来看一下anchor_spl::token中相关的代码片断:

#[derive(Clone)]
pub struct TokenAccount(spl_token::state::Account);

impl TokenAccount 
    pub const LEN: usize = spl_token::state::Account::LEN;


impl anchor_lang::AccountDeserialize for TokenAccount 
    fn try_deserialize(buf: &mut &[u8]) -> Result<Self, ProgramError> 
        TokenAccount::try_deserialize_unchecked(buf)
    

    fn try_deserialize_unchecked(buf: &mut &[u8]) -> Result<Self, ProgramError> 
        spl_token::state::Account::unpack(buf).map(TokenAccount)
    


impl anchor_lang::AccountSerialize for TokenAccount 
    fn try_serialize<W: Write>(&self, _writer: &mut W) -> Result<(), ProgramError> 
        // no-op
        Ok(())
    


impl anchor_lang::Owner for TokenAccount 
    fn owner() -> Pubkey 
        ID
    


impl Deref for TokenAccount 
    type Target = spl_token::state::Account;

    fn deref(&self) -> &Self::Target 
        &self.0
    

上面的代码最开始是定义了一个类元组结构体TokenAccount,其底层数据为spl_token::state::Account,也就是数据真实存储的数据结构,然后为了应用自动序列化与反序列化,实现了几个特型(也就是至少要实现第二节提到的AccountSerialize + AccountDeserialize + Owner + Clone

  • 第一个实现的是LEN字段。

  • 第二个实现的是AccountDeserialize特型,我们可以看到其直接调用了spl_token::state::Account::unpack 函数来将数据序列转化为一个Result<Account,ProgramError>,然后再将该结果使用一个map函数转为Result<TokenAccount, ProgramError>。这里我们可以看到,反序列化是直接调用原Account结构的相关方法,并没有手动去做解析。

  • 第三个实现的是AccountSerialize,因为我们不是spl-token,不是该账号的owner,并无权限写数据,所以不需要序例化,直接返回Ok(())就好。

  • 第四个实现的是Owner,它返回的是spl-token的programId

  • 第五个实现的是Deref,它用来修改*.在自定义类型上的行为,这里可以看到,直接引用时返回的是其内部数据Account的引用,这样我们就可以使用my_account.amount,有点类似Golang中直接读取内部结构体字段。因为我们不能修改其数据,所以并不需要实现 DerefMut

  • Clone,这个在结构声明时由系统自动实现。

这里可以看出,任何一个实现了AccountSerialize + AccountDeserialize + Owner + Clone的数据结构都可以应用在Anchor中的Account中。

那么问题来了,为什么不直接对spl-token::state::Account进行上面四个特型的实现呢?我们知道,实现特型是要么数据结构是内部的,要么特型定义是内部的,不能两个都来自于外部。原Account结构中的COption等是不支持Anchor的自动序列/反序列的,因此使用了一个自定义结构TokenAccount包装了一下Account,这样就可以在上面实现上述的四个特型了。

五、token-proxy

一般来讲,实际应用时涉及到spl-token的并不只是读取数据,基本上都会用来进行token交换的,这时交换双方的账号都需要标记为mut的,此时,不再适合将my_account定义为Account<'info, T>,更好的定义为AccountInfo<'info>,注意此时注解为#[account(mut)]。这时就不能再使用本文介绍的技巧来获取一些主要信息,如余额,mint和authority等。为此,anchor_spl::token还额外附加了一个模块anchor_spl::token::accessor,提供了三个基本方法来获取这些信息,代码如下:

// Field parsers to save compute. All account validation is assumed to be done
// outside of these methods.
pub mod accessor 
    use super::*;

    pub fn amount(account: &AccountInfo) -> Result<u64, ProgramError> 
        let bytes = account.try_borrow_data()?;
        let mut amount_bytes = [0u8; 8];
        amount_bytes.copy_from_slice(&bytes[64..72]);
        Ok(u64::from_le_bytes(amount_bytes))
    

    pub fn mint(account: &AccountInfo) -> Result<Pubkey, ProgramError> 
        let bytes = account.try_borrow_data()?;
        let mut mint_bytes = [0u8; 32];
        mint_bytes.copy_from_slice(&bytes[..32]);
        Ok(Pubkey::new_from_array(mint_bytes))
    

    pub fn authority(account: &AccountInfo) -> Result<Pubkey, ProgramError> 
        let bytes = account.try_borrow_data()?;
        let mut owner_bytes = [0u8; 32];
        owner_bytes.copy_from_slice(&bytes[32..64]);
        Ok(Pubkey::new_from_array(owner_bytes))
    

注释中也提到 ,未做账号有效性验证。希望应用的时候能注意。另外,这里的方式就是传统的读取数据字节来进行转换的方式。

其实,Anchor 专门为spl-token写了一个 token-proxy程序,用来使用anchor进行代币的转移等操作,见https://github.com/project-serum/anchor/blob/master/tests/spl/token-proxy/programs/token-proxy/src/lib.rs

六、结论

Anchor工具很强大,其项目方也写了很多适配传统版本Solana合约的示例代码及应用库,希望大家能使用它快速开发出自己的Solana Dapp。

以上是关于Solana中利用Anchor自动解析TokenAccount的主要内容,如果未能解决你的问题,请参考以下文章

在 Solana Anchor 框架中初始化 PDA 的正确方法

Solana Anchor:程序如何检查用户提供的已批准令牌配额?

Solana Anchor:如何为/读取关联帐户制作#[account(seeds)]?

Solana中的托管合约

使用 Anchor 时系统程序账号需要传入啥?

如何在 Serum Anchor 中签署代币交易