0
Help us understand the problem. What are the problem?

posted at

Solanaのアカウントをデコードしてみた

これは 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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
0
Help us understand the problem. What are the problem?