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。