Get Program Accounts

นี่ตือ RPC method ที่จะคืนค่า accounts ที่ program เป็นเจ้าของ. ในตอนนี้ยังไม่สนับสนุน pagination. การ requests ไปที่ getProgramAccounts จะต้องส่ง parameters dataSlice และ/หรือ filters ไปด้วยเพื่อลด response time และจะได้ส่งกลับมาเฉพาะผลลัพท์ที่ต้องการ.

เรื่องน่ารู้

Parameters

  • programId: string - Pubkey ของ program ที่จะ query, เตรียมในรูปแบบ base58 encoded string
  • (optional) configOrCommitment: object - Configuration parameters ที่มี optional fields ตามนี้:
    • (optional) commitment: string - State commitmentopen in new window
    • (optional) encoding: string - Encoding สำหรับ account data, ทั้ง: base58, base64, หรือ jsonParsed. Note, web3js ต้องใช้ getParsedProgramAccountsopen in new window แทน
    • (optional) dataSlice: object - จำกัดผลลัพท์ account data โดยขึ้นอยู่กับ:
      • offset: number - จำนวนของ bytes เริ่มต้นของ account data ที่จะเริ่มคืนค่ามา
      • length: number - จำนวนของ bytes ของ account data ที่จะส่งกลับมา
    • (optional) filters: array - คัดกรอง results โดยใช้ filter objects ข้างล่าง:
      • memcmp: object - ตรงกับ series ของ bytes ของ account data:
        • offset: number - จำนวนของ bytes เริ่มต้นของ account data ที่จะเทียบ
        • bytes: string - Data ที่จะเทียบด้วย, ในรูปแบบ base58 encoded string จำกัดที่ 129 bytes
      • dataSize: number - เทียบ account data length ด้วย data size ที่ระบุไว้
    • (optional) withContext: boolean - ครอบ (wrap)​ ผลลัพท์ในรูปแบบ RpcResponse JSON objectopen in new window
Response

ตามปกติแล้ว getProgramAccounts จะคืนค่า array ของ JSON objects ที่มีโครงสร้างตามนี้:

  • pubkey: string - account pubkey ในรูปแบบของ base58 encoded string
  • account: object - JSON object ที่มี fields:
    • lamports: number, ตัวเลขของ lamports ที่มีใน account
    • owner: string, base58 encoded pubkey ของ program ที่ account ได้ assigned ไว้
    • data: string | object - data ที่เกี่ยวข้องกับ account อาจจะเป็นได้ทั้ง encoded binary data หรือ JSON format ขึ้นอยู่กับ encoding parameter
    • executable: boolean, ตัวบ่งชี้ว่า account นี้มี program
    • rentEpoch: number, epoch ที่ account นี้จะต้องจ่าย rent

ลงลึก

getProgramAccounts คือ RPC method ที่จะคืนค่าทุก accounts ที่ program เป็นเจ้าของ. เราสามารถใช้ getProgramAccounts สำหรับดึงข้อมูลได้หลายแบบ เช่น:

  • หาทุกๆ token accounts ของ wallet
  • หาทุกๆ token accounts ที่มี mint เดียวกัน(เช่น ทุกๆ คนที่ถือ token SRMopen in new window ไว้)
  • หาทุกๆ custom accounts ที่ใช้ program นี้(เช่น ทุกๆ คนที่ใช้ Mangoopen in new window)

นอกจากจะมีประโยชน์แล้ว, getProgramAccounts ยังถูกเข้าใจผิดอยู่บ้าง เพราะด้วยข้อจำกัดของมัน การดึงข้อมูลที่ใช้ getProgramAccounts จะทำให้ RPC nodes ค้นหา data ขนาดใหญ่. การค้นหานั้นกินทั้ง memory และ resource มากๆ. ผลที่เกิดขึ้นคือถ้าเรียกใช้บ่อยเกินไป หรือใหญ่เกินไปจะทำให้เกิด connection timeouts ได้. ในตอนนี้ getProgramAccounts endpoint ยังไม่สนับสนุน pagination. ถ้าผลการค้นหาใหญ่เกินไปผลลัพท์จะถูกตัดทิ้ง.

เพื่อหลีกหนีข้อจำกัดนี้, getProgramAccounts เลยมี parameters ให้ใช้: ชื่อ, dataSlice และ filters options memcmp และ dataSize. ถ้าใช้ parameters เหล่านี้, เราจะสามารถลดขอบเขตของการค้นหาให้แคบลงเพื่อควบคุม และประมาณขนาดของผลลัพท์ได้.

ตัวอย่างทั่วไปของ getProgramAccounts ที่เกี่ยวข้องกับ SPL-Token Programopen in new window เช่น การค้นหาทุกๆ accounts ที่ Token Program เป็นเจ้าของโดยใช้ การค้นหาแบบปกติ จะทำให้ต้องไปค้นหาข้อมูลมากมาย แต่ถ้าเราใส่ parameters เข้าไปด้วยเราจะสามารถ request ได้อย่างประสิทธิภาพ และได้ data เฉพาะที่เราจะใช้.

filters

parameter ที่ใช้บ่อยๆ สำหรับ getProgramAccounts คือ filters array. ซึ่ง array นี้จะรับ filters 2 แบบคือ dataSize และ memcmp ก่อนที่จะใช้ filters นี้เราต้องรู้ก่อนว่า data ที่เราจะร้องขอมีรูปแบบยังไง และจัดเรียงไว้ยังไง.

dataSize

ในกรณีของ Token Program, เราจะเห็นว่า token accounts มีขนาด 165 bytesopen in new window. และ token account จะมี 8 fields ที่แตกต่างกันโดยแต่ละ field จะมีขนาด bytes ที่แน่นอน เราสามารถแสดง visualize ว่า data มีการวางรูปแบบยังไงโดยใช้รูปด้านล่าง.

Account Size

ถ้าเราต้องการหาทุกๆ token accounts โดยมี wallet address ของเราเป็นเจ้าของ, เราสามารถใส่ { dataSize: 165 } ใน filters เพื่อลดขอบเขตการค้นหาของเราให้เหลือเฉพาะ accounts ที่ขนาด 165 bytes เท่านั้น แต่เท่านี้ก็ยังไม่ดีพอ เราต้องต้องใส่ filter เข้าไปด้วยว่าเราเป็นเจ้าของ (owner) มันด้วย เราสามารถทำได้ด้วยการเพิ่ม memcmp filter เข้าไป.

memcmp

memcmp filter หรือ "memory comparison" filter, จะทำให้เราสามารถเปรียบเทียบ data ใน field ไหนก็ได้ที่เก็บอยู่ใน​ account ของเรา. โดยเฉพาะเราสามารถค้นหาเฉพาะ accounts ที่ตรงกับ bytes ที่ตำแหน่งใดๆ. memcmp ต้องการ 2 arguments:

  • offset: ตำแหน่งที่จะเริ่มเทียบ data มีขนาดเป็น bytes และแสดงเป็นจำนวนเต็ม.
  • bytes: คือ data ตรงกับ account's data. จะใช้ base-58 encoded string ขนาดไม่เกิน 129 bytes.

แต่ต้องระวังไว้ว่า memcmp จะคืนค่ามาก็ต่อเมื่อเจอ bytes ตรงกันเท่านั้น ซึ่งในตอนนี้เรายังไม่สามารถเทียบหาค่าที่น้อยกว่า หรือมากกว่า bytes ที่เราใส่ไปได้

ในตัวอย่าง Token Program อันต่อไป, เราสามารถกำหนดการค้นหาให้คืนค่ามาเฉพาะ token account ที่ตรงกับ wallet address ของเรา ถ้าเราลองดูที่ token account จะเห็นว่า 2 fields แรกบน token account คือ pubkeys, และแต่ละ pubkey จะมีขนาด 32 bytes โดยที่ owner จะอยู่ที่ field ที่ 2 เราจึงสามารถเริ่ม memcmp ที่ offset ที่ 32 bytes จากตรงนั้นเราก็สามารถมองหา account ที่ ower ตรงกับ wallet address ของเรา

Account Size

เราสามารถลอง query ได้ด้วยตัวอย่างด้านล่าง:

import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { clusterApiUrl, Connection } from "@solana/web3.js";

(async () => {
  const MY_WALLET_ADDRESS = "FriELggez2Dy3phZeHHAdpcoEXkKQVkv6tx3zDtCVP8T";
  const connection = new Connection(clusterApiUrl("devnet"), "confirmed");

  const accounts = await connection.getParsedProgramAccounts(
    TOKEN_PROGRAM_ID, // new PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
    {
      filters: [
        {
          dataSize: 165, // number of bytes
        },
        {
          memcmp: {
            offset: 32, // number of bytes
            bytes: MY_WALLET_ADDRESS, // base58 encoded string
          },
        },
      ],
    }
  );

  console.log(
    `Found ${accounts.length} token account(s) for wallet ${MY_WALLET_ADDRESS}: `
  );
  accounts.forEach((account, i) => {
    console.log(
      `-- Token Account Address ${i + 1}: ${account.pubkey.toString()} --`
    );
    console.log(`Mint: ${account.account.data["parsed"]["info"]["mint"]}`);
    console.log(
      `Amount: ${account.account.data["parsed"]["info"]["tokenAmount"]["uiAmount"]}`
    );
  });
  /*
    // Output

    Found 2 token account(s) for wallet FriELggez2Dy3phZeHHAdpcoEXkKQVkv6tx3zDtCVP8T: 
    -- Token Account Address 0:  H12yCcKLHFJFfohkeKiN8v3zgaLnUMwRcnJTyB4igAsy --
    Mint: CKKDsBT6KiT4GDKs3e39Ue9tDkhuGUKM3cC2a7pmV9YK
    Amount: 1
    -- Token Account Address 1:  Et3bNDxe2wP1yE5ao6mMvUByQUHg8nZTndpJNvfKLdCb --
    Mint: BUGuuhPsHpk8YZrL2GctsCtXGneL1gmT5zYb7eMHZDWf
    Amount: 3
  */
})();
use solana_client::{
  rpc_client::RpcClient, 
  rpc_filter::{RpcFilterType, Memcmp, MemcmpEncodedBytes, MemcmpEncoding},
  rpc_config::{RpcProgramAccountsConfig, RpcAccountInfoConfig},
};
use solana_sdk::{commitment_config::CommitmentConfig, program_pack::Pack};
use spl_token::{state::{Mint, Account}};
use solana_account_decoder::{UiAccountEncoding};

fn main() {
  const MY_WALLET_ADDRESS: &str = "FriELggez2Dy3phZeHHAdpcoEXkKQVkv6tx3zDtCVP8T";

  let rpc_url = String::from("http://api.devnet.solana.com");
  let connection = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed());

  let filters = Some(vec![
      RpcFilterType::Memcmp(Memcmp {
          offset: 32,
          bytes: MemcmpEncodedBytes::Base58(MY_WALLET_ADDRESS.to_string()),
          encoding: Some(MemcmpEncoding::Binary),
      }),
      RpcFilterType::DataSize(165),
  ]);

  let accounts = connection.get_program_accounts_with_config(
      &spl_token::ID,
      RpcProgramAccountsConfig {
          filters,
          account_config: RpcAccountInfoConfig {
              encoding: Some(UiAccountEncoding::Base64),
              commitment: Some(connection.commitment()),
              ..RpcAccountInfoConfig::default()
          },
          ..RpcProgramAccountsConfig::default()
      },
  ).unwrap();

  println!("Found {:?} token account(s) for wallet {MY_WALLET_ADDRESS}: ", accounts.len());

  for (i, account) in accounts.iter().enumerate() {
      println!("-- Token Account Address {:?}:  {:?} --", i, account.0);

      let mint_token_account = Account::unpack_from_slice(account.1.data.as_slice()).unwrap();
      println!("Mint: {:?}", mint_token_account.mint);

      let mint_account_data = connection.get_account_data(&mint_token_account.mint).unwrap();
      let mint = Mint::unpack_from_slice(mint_account_data.as_slice()).unwrap();
      println!("Amount: {:?}", mint_token_account.amount as f64 /10usize.pow(mint.decimals as u32) as f64);
  }
}

/*
// Output

Found 2 token account(s) for wallet FriELggez2Dy3phZeHHAdpcoEXkKQVkv6tx3zDtCVP8T: 
-- Token Account Address 0:  H12yCcKLHFJFfohkeKiN8v3zgaLnUMwRcnJTyB4igAsy --
Mint: CKKDsBT6KiT4GDKs3e39Ue9tDkhuGUKM3cC2a7pmV9YK
Amount: 1.0
-- Token Account Address 1:  Et3bNDxe2wP1yE5ao6mMvUByQUHg8nZTndpJNvfKLdCb --
Mint: BUGuuhPsHpk8YZrL2GctsCtXGneL1gmT5zYb7eMHZDWf
Amount: 3.0
*/
curl http://api.mainnet-beta.solana.com -X POST -H "Content-Type: application/json" -d '
  {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "getProgramAccounts",
    "params": [
      "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
      {
        "encoding": "jsonParsed",
        "filters": [
          {
            "dataSize": 165
          },
          {
            "memcmp": {
              "offset": 32,
              "bytes": "FriELggez2Dy3phZeHHAdpcoEXkKQVkv6tx3zDtCVP8T"
            }
          }
        ]
      }
    ]
  }
'

# Output: 
# {
#   "jsonrpc": "2.0",
#   "result": [
#     {
#       "account": {
#         "data": {
#           "parsed": {
#             "info": {
#               "isNative": false,
#               "mint": "BUGuuhPsHpk8YZrL2GctsCtXGneL1gmT5zYb7eMHZDWf",
#               "owner": "FriELggez2Dy3phZeHHAdpcoEXkKQVkv6tx3zDtCVP8T",
#               "state": "initialized",
#               "tokenAmount": {
#                 "amount": "998999999000000000",
#                 "decimals": 9,
#                 "uiAmount": 998999999,
#                 "uiAmountString": "998999999"
#               }
#             },
#             "type": "account"
#           },
#           "program": "spl-token",
#           "space": 165
#         },
#         "executable": false,
#         "lamports": 2039280,
#         "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
#         "rentEpoch": 313
#       },
#       "pubkey": "Et3bNDxe2wP1yE5ao6mMvUByQUHg8nZTndpJNvfKLdCb"
#     }
#   ],
#   "id": 1
# }

dataSlice

นอกจาก 2 filter parameters นี้แล้ว, parameter ที่ 3 ที่ใช้บ่อยสำหรับ getProgramAccounts ก็คือ dataSlice แต่จะไม่เหมือน parameter filters ตรงที่ dataSlice จะไม่ลดผลการค้นหาของ accounts แต่ dataSlice จะจำกัดจำนวน data ที่ค้นหาได้แทน

คล้ายๆ memcmp, dataSlice จะรับ 2 arguments ดังนี้:

  • offset: ตำแหน่ง (ในขนาดของ bytes) ที่เริ่มคืนค่า account data
  • length: จำนวนของ bytes ที่จะได้กลับคืนมา

dataSlice ใช้ได้ดีเวลาค้นหาข้อมูลขนาดใหญ่โดยไม่สนใจ data ตัวอย่างเช่น เวลาที่เราต้องการนับจำนวนของ token accounts (เช่น จำนวนคนที่ถือ token) สำหรับ token mint ที่เราสนใจ

import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { clusterApiUrl, Connection } from "@solana/web3.js";

(async () => {
  const MY_TOKEN_MINT_ADDRESS = "BUGuuhPsHpk8YZrL2GctsCtXGneL1gmT5zYb7eMHZDWf";
  const connection = new Connection(clusterApiUrl("devnet"), "confirmed");

  const accounts = await connection.getProgramAccounts(
    TOKEN_PROGRAM_ID, // new PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
    {
      dataSlice: {
        offset: 0, // number of bytes
        length: 0, // number of bytes
      },
      filters: [
        {
          dataSize: 165, // number of bytes
        },
        {
          memcmp: {
            offset: 0, // number of bytes
            bytes: MY_TOKEN_MINT_ADDRESS, // base58 encoded string
          },
        },
      ],
    }
  );
  console.log(
    `Found ${accounts.length} token account(s) for mint ${MY_TOKEN_MINT_ADDRESS}`
  );
  console.log(accounts);

  /*
  // Output (notice the empty <Buffer > at acccount.data)
  
  Found 3 token account(s) for mint BUGuuhPsHpk8YZrL2GctsCtXGneL1gmT5zYb7eMHZDWf
  [
    {
      account: {
        data: <Buffer >,
        executable: false,
        lamports: 2039280,
        owner: [PublicKey],
        rentEpoch: 228
      },
      pubkey: PublicKey {
        _bn: <BN: a8aca7a3132e74db2ca37bfcd66f4450f4631a5464b62fffbd83c48ef814d8d7>
      }
    },
    {
      account: {
        data: <Buffer >,
        executable: false,
        lamports: 2039280,
        owner: [PublicKey],
        rentEpoch: 228
      },
      pubkey: PublicKey {
        _bn: <BN: ce3b7b906c2ff6c6b62dc4798136ec017611078443918b2fad1cadff3c2e0448>
      }
    },
    {
      account: {
        data: <Buffer >,
        executable: false,
        lamports: 2039280,
        owner: [PublicKey],
        rentEpoch: 228
      },
      pubkey: PublicKey {
        _bn: <BN: d4560e42cb24472b0e1203ff4b0079d6452b19367b701643fa4ac33e0501cb1>
      }
    }
  ]
  */
})();
use solana_client::{
  rpc_client::RpcClient, 
  rpc_filter::{RpcFilterType, Memcmp, MemcmpEncodedBytes, MemcmpEncoding},
  rpc_config::{RpcProgramAccountsConfig, RpcAccountInfoConfig},
};
use solana_sdk::{commitment_config::CommitmentConfig};
use solana_account_decoder::{UiAccountEncoding, UiDataSliceConfig};

pub fn main() {
  const MY_TOKEN_MINT_ADDRESS: &str = "BUGuuhPsHpk8YZrL2GctsCtXGneL1gmT5zYb7eMHZDWf";

  let rpc_url = String::from("http://api.devnet.solana.com");
  let connection = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed());

  let filters = Some(vec![
      RpcFilterType::Memcmp(Memcmp {
          offset: 0, // number of bytes
          bytes: MemcmpEncodedBytes::Base58(MY_TOKEN_MINT_ADDRESS.to_string()),
          encoding: Some(MemcmpEncoding::Binary),
      }),
      RpcFilterType::DataSize(165), // number of bytes
  ]);

  let accounts = connection.get_program_accounts_with_config(
      &spl_token::ID,
      RpcProgramAccountsConfig {
          filters,
          account_config: RpcAccountInfoConfig {
              data_slice: Some(UiDataSliceConfig {
                  offset: 0, // number of bytes
                  length: 0, // number of bytes
              }),
              encoding: Some(UiAccountEncoding::Base64),
              commitment: Some(connection.commitment()),
              ..RpcAccountInfoConfig::default()
          },
          ..RpcProgramAccountsConfig::default()
      },
  ).unwrap();

  println!("Found {:?} token account(s) for mint {MY_TOKEN_MINT_ADDRESS}: ", accounts.len());
  println!("{:#?}", accounts);
}

/*
// Output (notice the empty <Buffer > at acccount.data)

Found 3 token account(s) for mint BUGuuhPsHpk8YZrL2GctsCtXGneL1gmT5zYb7eMHZDWf: 
[
  (
      tofD3NzLfZ5pWG91JcnbfsAbfMcFF2SRRp3ChnjeTcL,
      Account {
          lamports: 2039280,
          data.len: 0,
          owner: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA,
          executable: false,
          rent_epoch: 319,
      },
  ),
  (
      CMSC2GeWDsTPjfnhzCZHEqGRjKseBhrWaC2zNcfQQuGS,
      Account {
          lamports: 2039280,
          data.len: 0,
          owner: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA,
          executable: false,
          rent_epoch: 318,
      },
  ),
  (
      Et3bNDxe2wP1yE5ao6mMvUByQUHg8nZTndpJNvfKLdCb,
      Account {
          lamports: 2039280,
          data.len: 0,
          owner: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA,
          executable: false,
          rent_epoch: 318,
      },
  ),
]
*/
# Note: encoding only available for "base58", "base64" or "base64+zstd"
curl http://api.mainnet-beta.solana.com -X POST -H "Content-Type: application/json" -d '
  {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "getProgramAccounts",
    "params": [
      "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
      {
        "encoding": "base64",
        "dataSlice": {
          "offset": 0,
          "length": 0
        },
        "filters": [
          {
            "dataSize": 165
          },
          {
            "memcmp": {
              "offset": 0,
              "bytes": "BUGuuhPsHpk8YZrL2GctsCtXGneL1gmT5zYb7eMHZDWf"
            }
          }
        ]
      }
    ]
  }
'

# Output:
# {
#   "jsonrpc": "2.0",
#   "result": [
#     {
#       "account": {
#         "data": [
#           "",
#           "base64"
#         ],
#         "executable": false,
#         "lamports": 2039280,
#         "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
#         "rentEpoch": 313
#       },
#       "pubkey": "FqWyVSLQgyRWyG1FuUGtHdTQHrEaBzXh1y9K6uPVTRZ4"
#     },
#     {
#       "account": {
#         "data": [
#           "",
#           "base64"
#         ],
#         "executable": false,
#         "lamports": 2039280,
#         "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
#         "rentEpoch": 314
#       },
#       "pubkey": "CMSC2GeWDsTPjfnhzCZHEqGRjKseBhrWaC2zNcfQQuGS"
#     },
#     {
#       "account": {
#         "data": [
#           "",
#           "base64"
#         ],
#         "executable": false,
#         "lamports": 2039280,
#         "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
#         "rentEpoch": 314
#       },
#       "pubkey": "61NfACb21WvuEzxyiJoxBrivpiLQ79gLBxzFo85BiJ2U"
#     },
#     {
#       "account": {
#         "data": [
#           "",
#           "base64"
#         ],
#         "executable": false,
#         "lamports": 2039280,
#         "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
#         "rentEpoch": 313
#       },
#       "pubkey": "Et3bNDxe2wP1yE5ao6mMvUByQUHg8nZTndpJNvfKLdCb"
#     }
#   ],
#   "id": 1
# }

โดนการที่เราผสม 3 parameters (dataSlice, dataSize, และ memcmp) เราก็จะสามารถจำกัดการค้นหาให้มีประสิทธิภาพ และส่งค่ากลับมาเฉพาะที่เราต้องการได้

แหล่งข้อมูลอื่น

Last Updated: