これは Solana Advent Calendar 2021 の記事です。
#アカウントのデコード方法
学習のため、Solanaのアカウントのデータ部分をデコードする方法を探していたら以下のようなクレートを発見しました。
本記事ではこれを利用して、Solanaの基本的なアカウントをデコードするRustプログラムを作ってみました。アカウントにはいろいろな種類がありますが、デコードの対象は以下のようなアカウントです。
- プログラムアカウント
- 設定アカウント(バリデータ情報等)
- Nonceアカウント
- SPLトークンアカウント
- Sysvarアカウント(Clock, Fees, Rent等)
- ステークアカウント
- 投票アカウント
当然ながら、アプリケーション固有のデータ(Serumの板情報等)を含むようなアカウントはこの方法ではデコードできません。
また、以降では、SPLトークンアカウントのうち、トークン自体の情報を持つアカウントを「トークンミントアカウント」、トークンの残高を保持するアカウントを「トークンアカウント」と呼んで区別します。
Cargo.toml
[package]
name = "solana-account"
version = "0.1.0"
edition = "2021"
[dependencies]
solana-sdk = "1.9.0"
solana-client = "1.9.0"
solana-account-decoder = "1.9.0"
solana-account.rs
use solana_account_decoder::parse_account_data::{
parse_account_data, AccountAdditionalData, ParseAccountError, ParsedAccount,
};
use solana_client::rpc_client::RpcClient;
use solana_sdk::{account::Account, commitment_config::CommitmentConfig, pubkey::Pubkey};
use std::env;
use std::str::FromStr;
fn main() {
let args: Vec<String> = env::args().collect();
let cluster = args[1].as_str();
let addr = &args[2];
let url = match cluster {
"localhost" => "http://localhost:8899",
"devnet" => "https://api.devnet.solana.com",
"testnet" => "https://api.testnet.solana.com",
"mainnet-beta" => "https://api.mainnet-beta.solana.com",
_ => "https://api.mainnet-beta.solana.com",
};
let rpc_client = RpcClient::new_with_commitment(url.to_string(), CommitmentConfig::confirmed());
println!(
"Connected to {}, slot={}",
cluster,
rpc_client.get_slot().unwrap()
);
let pubkey = Pubkey::from_str(addr).expect("Invalid address");
// https://github.com/solana-labs/solana/blob/master/client/src/rpc_client.rs#L3544
let account: Account = rpc_client.get_account(&pubkey).unwrap();
let balance = (account.lamports as f64) / 10_f64.powi(9);
let owner: Pubkey = account.owner;
let account_data = account.data;
println!("Public Key: {}", pubkey);
println!("Balance: {:.9} SOL", balance);
println!("Owner: {}", owner);
println!("Executable: {}", account.executable);
println!("Rent Epoch: {}", account.rent_epoch);
if account_data.len() == 0 {
return;
}
let result = parse_account_data(&pubkey, &owner, &account_data, None);
match result {
Ok(v) => println!("{:#?}", v),
Err(e) => match e {
// トークンアカウントの場合はdecimalが不明なので、別途デコードする
ParseAccountError::AdditionalDataMissing(_s) => {
parse_spl_token_account_data(&rpc_client, &pubkey, &owner, &account_data)
}
_ => println!("Error: {}", e),
},
};
}
fn parse_spl_token_account_data(
client: &RpcClient,
pubkey: &Pubkey,
program_id: &Pubkey,
data: &[u8],
) {
// トークンアカウントに紐付いたトークンミントアカウントのアドレスを取得する
let opt: Option<AccountAdditionalData> = Some(AccountAdditionalData {
spl_token_decimals: Some(0),
});
let result: ParsedAccount = parse_account_data(&pubkey, &program_id, &data, opt).unwrap();
let mint_address = result
.parsed
.get("info")
.unwrap()
.get("mint")
.unwrap()
.as_str()
.unwrap();
let mint_pubkey = Pubkey::from_str(&mint_address).expect("Invalid address");
// トークンミントアカウントからdecimalsを取得する
let mint_account: Account = client.get_account(&mint_pubkey).unwrap();
let result: ParsedAccount =
parse_account_data(&mint_pubkey, &mint_account.owner, &mint_account.data, None).unwrap();
let decimals = result
.parsed
.get("info")
.unwrap()
.get("decimals")
.unwrap()
.as_u64()
.unwrap() as u8;
// decimalsの値を渡して、改めてトークンアカウントのデコードを行う
let opt: Option<AccountAdditionalData> = Some(AccountAdditionalData {
spl_token_decimals: Some(decimals),
});
let result: ParsedAccount = parse_account_data(&pubkey, &program_id, &data, opt).unwrap();
println!("{:#?}", result);
}
第1引数ではクラスタの選択を行います。mainnet-beta、devnet、testnet、localhostの中から選んでください。デコードしたいアカウントのアドレスは第2引数に指定します。
実行例
cargo buildして実行してみる。
プログラムアカウント
> target/debug/solana-account mainnet-beta 9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9P
usVFin
Connected to mainnet-beta, slot=112882743
Public Key: 9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin
Balance: 0.001141440 SOL
Owner: BPFLoaderUpgradeab1e11111111111111111111111
Executable: true
Rent Epoch: 148
ParsedAccount {
program: "bpf-upgradeable-loader",
parsed: Object({
"info": Object({
"programData": String(
"DTxcpNApaMLNfYgwQ99PmpCm8rjS7o1q2YdfzYsrYohB",
),
}),
"type": String(
"program",
),
}),
space: 36,
}
トークンミントアカウント
> target/debug/solana-account mainnet-beta So111111111111111111111111111111111111
11112
Connected to mainnet-beta, slot=112881984
Public Key: So11111111111111111111111111111111111111112
Balance: 151.330216991 SOL
Owner: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
Executable: false
Rent Epoch: 261
ParsedAccount {
program: "spl-token",
parsed: Object({
"info": Object({
"decimals": Number(
9,
),
"freezeAuthority": Null,
"isInitialized": Bool(
true,
),
"mintAuthority": Null,
"supply": String(
"0",
),
}),
"type": String(
"mint",
),
}),
space: 82,
}
トークンアカウント
> target/debug/solana-account mainnet-beta 36c6YqAwyGKQG66XEp2dJc5JqjaBNv7sVghEtJv4c7u6
Connected to mainnet-beta, slot=112882502
Public Key: 36c6YqAwyGKQG66XEp2dJc5JqjaBNv7sVghEtJv4c7u6
Balance: 839783.012039280 SOL
Owner: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
Executable: false
Rent Epoch: 261
ParsedAccount {
program: "spl-token",
parsed: Object({
"info": Object({
"isNative": Bool(
true,
),
"mint": String(
"So11111111111111111111111111111111111111112",
),
"owner": String(
"F8Vyqk3unwxkXukZFQeYyGmFfTG3CAX4v24iyrjEYBJV",
),
"rentExemptReserve": Object({
"amount": String(
"2039280",
),
"decimals": Number(
9,
),
"uiAmount": Number(
0.00203928,
),
"uiAmountString": String(
"0.00203928",
),
}),
"state": String(
"initialized",
),
"tokenAmount": Object({
"amount": String(
"839783000000000",
),
"decimals": Number(
9,
),
"uiAmount": Number(
839783.0,
),
"uiAmountString": String(
"839783",
),
}),
}),
"type": String(
"account",
),
}),
space: 165,
}
Clock(Sysvar)アカウント
> target/debug/solana-account mainnet-beta SysvarC1ock11111111111111111111111111111111
Connected to mainnet-beta, slot=112882594
Public Key: SysvarC1ock11111111111111111111111111111111
Balance: 0.001169280 SOL
Owner: Sysvar1111111111111111111111111111111111111
Executable: false
Rent Epoch: 261
ParsedAccount {
program: "sysvar",
parsed: Object({
"info": Object({
"epoch": Number(
261,
),
"epochStartTimestamp": Number(
1640180883,
),
"leaderScheduleEpoch": Number(
262,
),
"slot": Number(
112882595,
),
"unixTimestamp": Number(
1640252488,
),
}),
"type": String(
"clock",
),
}),
space: 40,
}
SOLアドレス
3reEHqvUKcSmWCzDPiY2nV9Y4q1qAdu3rzYa8UX64Bvk