Đọ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 commitment - (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 getParsedProgramAccounts - (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 Accountlength
: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 Accountoffset
:number
- Vị trí bắt đầu trong dữ liệu Account dùng để so sánhbytes
: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 JSON
- (Tuỳ chọn)
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á base58account
:object
- Là một đối tượng JSON với các trường con như sau:lamports
:number
, số dư lamports của Accountowner
:string
, Địa chỉ của Program sở hữu Account và được mã hoá base58data
: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àoexecutable
:boolean
, Nhãn đánh dấu nếu Account này chứa một Program và có thể thực thirentEpoch
: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 SRM)
- 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 Mango)
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 memcmp
và dataSize
. 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 Program. 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à dataSize
và memcmp
. 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 bytes. Đặ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.
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 }
và 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
.
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 {
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
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 {
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
# }
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.