Đọc Program Account

getProgramAccounts là một phương thức RPC giúp lấy dữ liệu của tất cả các Account được sở hữu bởi Program. Lưu ý, phân trang vẫn chưa được hỗ trợ tại thời điểm hiện tại. Việc gọi getProgramAccounts nên có thêm các tham số dataSlice và/hoặc filters để cải thiện thời gian trả về với kết quả mong muốn.

Có thể bạn chưa biết

Tham số

  • programId: string - Khoá công khai của Program cần truy vấn và biểu diễn dưới dạng base58
  • (Tuỳ chọn) configOrCommitment: object - Tham số cài đặt có chứa các trường tuỳ chọn sau:
    • (Tuỳ chọn) commitment: string - State commitmentopen in new window
    • (Tuỳ chọn) encoding: string - Kiểu mã hoá dữ liệu, một trong các kiểu sau: base58, base64, jsonParsed. Lưu ý, người dùng web3js nên sử dụng getParsedProgramAccountsopen in new window
    • (Tuỳ chọn) dataSlice: object - Giới hạn các Account trả về dựa trên:
      • offset: number - Vị trí bắt đầu cho dữ liệu được trả về của Account
      • length: number - Độ dài dữ liệu của Account cần trả về và được tính từ vị trí bắt đầu
    • (Tuỳ chọn) filters: array - Lọc các kết quả bằng cách sử dụng các bộ lọc sau:
      • memcmp: object - Lọc bằng cách so sánh một chuỗi dữ liệu dưới dạng các bytes với dữ liệu Account
        • offset: number - Vị trí bắt đầu trong dữ liệu Account dùng để so sánh
        • bytes: string - Dữ liệu cần so sánh, được truyền vào dưới dạng base58 và không quá 129 bytes
      • dataSize: number - Lọc theo độ lớn của dữ liệu Account
    • (Tuỳ chọn) withContext: boolean - Đóng gói kết quả vào một đối tượng RpcResponse JSONopen in new window
Trả về

Mặc định getProgramAccounts sẽ trả về một mảng các đối tượng JSON với cấu trúc như sau:

  • pubkey: string - Địa chỉ của Account và được mã hoá base58
  • account: object - Là một đối tượng JSON với các trường con như sau:
    • lamports: number, số dư lamports của Account
    • owner: string, Địa chỉ của Program sở hữu Account và được mã hoá base58
    • data: string | object - Dữ liệu của Account và được biểu diễn dưới dạng, hoặc là binary, hoặc là JSON, tuỳ vào tham số encoding lúc truyền vào
    • executable: boolean, Nhãn đánh dấu nếu Account này chứa một Program và có thể thực thi
    • rentEpoch: number, Kỳ hạn thuê tiếp theo của Account

Chi tiết

getProgramAccounts là một phương thức RPC rất linh hoạt và có khả năng trả về tất cả các Account được sở hữu bởi một Program. Chúng ta có thể sử dụng getProgramAccounts cho nhiều loại truy vấn khác nhau, ví dụ như:

  • Tất cả các Account của một ví cụ thể
  • Tất cả các Account cho một mint (hoặc thường được gọi là token đối với các blockchain khác) (i.e. Tất cả người giữ token SRMopen in new window)
  • Tất cả các Account theo ý muốn của một Program cụ thể (i.e. Tất cả Account người dùng của ứng dụng Mangoopen in new window)

Mặc dù hữu dụng là vậy, getProgramAccounts thường bị dùng sai vì các hạn chế hiện tại. Nhiều câu truy vấn được hỗ trợ bởi getProgramAccounts yêu cầu các nốt RPC phải quét một khối lượng rất lớn các dữ liệu. Những câu truy vấn như vậy không chỉ lớn về dung lượng dữ liệu và còn lớn về khối lượng tính toán. Tất yếu, việc gọi quá nhiều về cả tần suất và khối lượng dẫn đến kết nối sẽ bị ngắt. Ngoài ra, tại thời điểm cuốn sách được viết, getProgramAccounts vẫn chưa hỗ trợ phân trang. Nếu kết quả truy vấn quá lớn, nó sẽ được cắt bỏ đi.

Để tránh các hạn chế này, getProgramAccounts giới thiệu các tham số dùng cho việc lọc và sơ chế kết quả, ví dụ như: dataSlice, filters với tuỳ chọn memcmpdataSize. Bằng cách kết hợp các tham số trên, chúng ta có thể giảm thiểu phạm vi truy vấn với kích thước dữ liệu được kiểm soát và dễ đoán hơn.

Một ví dụ thường thấy của getProgramAccounts là tương tác với SPL-Token Programopen in new window. Truy vấn tất cả các Account được sở hữu bởi Token Program với một câu truy vấn thuần tuý không có lọc sẽ dẫn đến một số lượng dữ liệu trả về khổng lồ. Thay vào đó, bằng cách bổ sung các tham số, chúng ta có thể truy vấn một cách hiệu quả chỉ những dữ liệu mình cần.

filters

Tham số phổ biến nhất được dùng kèm với getProgramAccounts chính là mảng các filters. Mảng này chấp nhận 2 kiểu lọc là dataSizememcmp. Trước khi sử dụng một trong hai, chúng ta nên hiểu được dữ liệu cần truy vấn sẽ có chứa dữ liệu gì? hình thái ra sao? tuần tự hoá như thế nào?

dataSize

Trong trường hợp Token Program, chúng ta có thể thấy rằng độ dài của Token Account là 165 bytesopen in new window. Đặc biệt, một Token Account có 8 trường con, với mỗi trường có độ dài vùng nhớ biết trước. Chúng ta có thể mường tượng cách dữ liệu được sắp xếp bằng minh hoạ sau.

Account Size

Nếu chúng ta muốn tìm tất cả Token Account sở hữu bởi chỉ riêng ví của mình, chúng ta có thể thêm { dataSize: 165 }filters để thu hẹp pham vi câu truy vấn và chỉ lấy những Account có độ dài chính xác 165 bytes. Tuy vậy, nó vẫn là chưa đủ. Chúng ta cần thêm một điều kiện để chỉ lọc các Account được sở hữu bởi ví của mình. Để là được điều đó, chúng ta phải sử dụng memcmp.

memcmp

Điều kiện lọc memcmp, hoặc "memory comparison" (phép so sánh vùng nhớ), cho phép chúng ta so sánh dữ liệu truyền vào với bất kỳ vùng nhớ nào được lưu trong Account. Đặc biệt, chúng ta có thể truy vấn chỉ những Account mà khớp với một đoạn dữ liệu tại một vị trí cụ thể. memcmp yêu cầu 2 tham số:

  • offset: Vị trí bắt đầu để so sánh dữ liệu. Vị trí này thường được tính theo bytes và biểu diễn dưới dạng số nguyên.
  • bytes: Dữ liệu dùng để đối chiếu với dữ liệu trong Account. Dữ liệu này nên được biểu diễn dưới dạng base58 và không quá 129 bytes.

Một điều quan trọng cần lưu ý là memcmp chỉ trả về các kết quả khớp chính xác trên từng bytes. Và hiện tại không hỗ trợ các phép so sánh lớn hơn hoặc nhỏ hơn cho bytes.

Sử dụng lại ví dụ Token Program bên trên, chúng ta điều chỉnh câu truy vấn chỉ trả về những Token Account mà được sở hữu bởi chính mình. Khi nhìn vào một Token Account, chúng ta biết được 2 trường đầu tiên lưu trong Token Account là 2 khoá công khai với độ dài là 32 bytes. Biết rằng owner là trường thứ 2, chúng ta nên khởi tạo memcmp với offset là 32. Từ đó, chúng ta sẽ lọc được những Account của mình bằng cách truyền địa chỉ ví vào bytes.

Account Size

Chúng ta có thể gọi câu truy vấn này thông qua ví dụ sau:

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::new(
        32, // number of bytes
        MemcmpEncodedBytes::Base58(MY_WALLET_ADDRESS.to_string()),
      )),
      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

Ngoài 2 tham số bộ lọc được nhắc đến ở trên, một tham số thứ 3 cho getProgramAccounts cũng phổ biến không kém đó là dataSlice. Không giống như filters, dataSlice sẽ không giảm số lượng Account trả về. Thay vào đó, dataSlice sẽ giúp giới hạn số lượng dữ liệu trả về trên mỗi Account.

Cũng giống với memcmp, dataSlice có 2 tham số con:

  • offset: Vị trí bắt đầu của dữ liệu mong muốn trả về
  • length: Số lượng bytes trả về tính từ vị trí bắt đầu

dataSlice rất hữu ích trong thực tế khi mà chúng ta có thể truy vấn một khối lượng lớn dữ liệu đồng thời bỏ qua các trường không cần thiết trong dữ liệu Account. Một ví dụ cho trường hợp này là khi chúng ta muốn tính số lượng Token Account (cụ thể là số người nắm giữ token) cho một mint cụ thể.

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::new(
        0, // number of bytes
        MemcmpEncodedBytes::Base58(MY_TOKEN_MINT_ADDRESS.to_string()),
      )),
      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 zero `len` in `data` of `Account`s)

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
# }

Vời việc kết hợp giữ 3 tham số (dataSlice, dataSize, và memcmp), chúng ta có thể giới hạn phạm vi truy vấn một cách hiệu quả với chỉ các kết quả trả về mà chúng ta quan tâm.

Các nguồn tài liệu khác

Last Updated:
Contributors: Trần Minh Quang, tuphan-dn