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

これは [Solana Advent Calendar 2021](https://adventar.org/calendars/6174) の記事です。

#アカウントのデコード方法

学習のため、Solanaのアカウントのデータ部分をデコードする方法を探していたら以下のようなクレートを発見しました。

https://crates.io/crates/solana-account-decoder/

本記事ではこれを利用して、Solanaの基本的なアカウントをデコードするRustプログラムを作ってみました。アカウントにはいろいろな種類がありますが、デコードの対象は以下のようなアカウントです。

* プログラムアカウント
* 設定アカウント(バリデータ情報等)
* Nonceアカウント
* SPLトークンアカウント
* Sysvarアカウント(Clock, Fees, Rent等)
* ステークアカウント
* 投票アカウント

当然ながら、アプリケーション固有のデータ(Serumの板情報等)を含むようなアカウントはこの方法ではデコードできません。
また、以降では、SPLトークンアカウントのうち、トークン自体の情報を持つアカウントを「トークンミントアカウント」、トークンの残高を保持するアカウントを「トークンアカウント」と呼んで区別します。

```toml: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"
```

```rust: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