🙄

Pyth NetworkのAccount調査

2024/01/15に公開

Solanaで完結した状態で各価格情報を欲しかったのでその調査メモ
https://docs.pyth.network/documentation

Accountデータの構造

https://docs.pyth.network/documentation/how-pyth-works/account-structure
https://docs.pyth.network/documentation/how-pyth-works/product-metadata

詳細は上記を参照すること。

AccountデータはFsJ3A3u2vn5cTVofAjvy6y5kwABJAqYWpe4975bi2epHのプログラムで作られるデータであり、getProgramAccountsで一括取得が可能。ただし、データ量が大きいため専用のノードは必要になる。

Mappingデータ

https://github.com/pyth-network/pyth-sdk-rs/blob/main/pyth-sdk-solana/src/state.rs#L146-L164

ProductデータへのN件の参照(Pubkey)を持つ構造。
Mapping → Mappingの連結リスト構造を持ち、Mappingの持つProductデータへの参照が溢れたら次のMappingを作りリストに追加する。

https://github.com/pyth-network/pyth-client-js/blob/8c24cfc63dad1f942768db0dc425f489b0294162/src/index.ts#L157-L189

Accountはbytemuckでバイト化されており、client側を見て分かる通りstruct構造そのままの形になっている。
保存されるバイトはLE(リトルエンディアン)なのに注意

Productデータ

https://github.com/pyth-network/pyth-sdk-rs/blob/main/pyth-sdk-solana/src/state.rs#L175-L192

通貨のペア情報を持ち、1件のPriceデータの参照(Pubkey)を持つ構造。

https://github.com/pyth-network/pyth-client-js/blob/8c24cfc63dad1f942768db0dc425f489b0294162/src/index.ts#L191-L220

ペア情報はPROD_ATTR_SIZEの長さの動的なKey-Valueであり、KeyとValueそれぞれの先頭バイトに長さが入っている。

https://github.com/pyth-network/pyth-client-js/blob/8c24cfc63dad1f942768db0dc425f489b0294162/src/index.ts#L59-L66

プログラム/クライアント側の型定義を見て分かる通り、このペア情報は型がなく何が入るかは不明。(というほどでもないけど、KVの追加、削除が起こり得るので信用はできない)

この動的な性質からgetProgramAccountsfilterなどで特定の通貨ペアの情報だけを取得するなどはできず、全ての情報を取得する必要がある。Hermes経由なら取得できるかもしれないが興味ないので調べていない。

Priceデータ

https://github.com/pyth-network/pyth-sdk-rs/blob/main/pyth-sdk-solana/src/state.rs#L208-L237

通貨ペアの価格情報を持つ構造。

https://github.com/pyth-network/pyth-client-js/blob/8c24cfc63dad1f942768db0dc425f489b0294162/src/index.ts#L257-L370

pyth-sdk-rsとpyth-client-jsで微妙に型定義が違いそうに見える。

Price Feed ID一覧

https://pyth.network/developers/price-feed-ids#solana-mainnet-beta

Account Address一覧

https://pyth.network/developers/accounts?cluster=solana-mainnet-beta

getProgramAccountsで絞り込み付きで取得する参考実装

const u32 = (n: number) => {
  const buffer = new ArrayBuffer(4);
  const dataView = new DataView(buffer);
  dataView.setUint32(0, n, true);
  const array = new Uint8Array(buffer);
  return array
};
const connection = useConnection();
const accounts = await connection.getProgramAccounts(new PublicKey("FsJ3A3u2vn5cTVofAjvy6y5kwABJAqYWpe4975bi2epH"), {
  filters: [
    {
      memcmp: {
        offset: 0,
        bytes: bs58.encode(u32(Magic)),
      },
    },
    {
      memcmp: {
        offset: 4,
        bytes: bs58.encode(u32(Version2)),
      },
    },
    {
      memcmp: {
        offset: 8,
        bytes: bs58.encode(u32(AccountType.Product)),
      },
    }
  ]
});
accounts.forEach(({account, pubkey}) => {
  const product = parseProductData(account.data);
  console.log('pubkey', pubkey.toString());
  console.log('product', product);
});

3つ目のmemcmpの値を変える、parseProductDataを対応するparseに変更することで他のデータも取得できる。

Discussion