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 commitment - (optional)
encoding
:string
- Encoding สำหรับ account data, ทั้ง:base58
,base64
, หรือjsonParsed
. Note, web3js ต้องใช้ getParsedProgramAccounts แทน - (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 object
- (optional)
Response
ตามปกติแล้ว getProgramAccounts
จะคืนค่า array ของ JSON objects ที่มีโครงสร้างตามนี้:
pubkey
:string
- account pubkey ในรูปแบบของ base58 encoded stringaccount
:object
- JSON object ที่มี fields:lamports
:number
, ตัวเลขของ lamports ที่มีใน accountowner
:string
, base58 encoded pubkey ของ program ที่ account ได้ assigned ไว้data
:string
|object
- data ที่เกี่ยวข้องกับ account อาจจะเป็นได้ทั้ง encoded binary data หรือ JSON format ขึ้นอยู่กับ encoding parameterexecutable
:boolean
, ตัวบ่งชี้ว่า account นี้มี programrentEpoch
:number
, epoch ที่ account นี้จะต้องจ่าย rent
ลงลึก
getProgramAccounts
คือ RPC method ที่จะคืนค่าทุก accounts ที่ program เป็นเจ้าของ. เราสามารถใช้ getProgramAccounts
สำหรับดึงข้อมูลได้หลายแบบ เช่น:
- หาทุกๆ token accounts ของ wallet
- หาทุกๆ token accounts ที่มี mint เดียวกัน(เช่น ทุกๆ คนที่ถือ token SRM ไว้)
- หาทุกๆ custom accounts ที่ใช้ program นี้(เช่น ทุกๆ คนที่ใช้ Mango)
นอกจากจะมีประโยชน์แล้ว, getProgramAccounts
ยังถูกเข้าใจผิดอยู่บ้าง เพราะด้วยข้อจำกัดของมัน การดึงข้อมูลที่ใช้ getProgramAccounts
จะทำให้ RPC nodes ค้นหา data ขนาดใหญ่. การค้นหานั้นกินทั้ง memory และ resource มากๆ. ผลที่เกิดขึ้นคือถ้าเรียกใช้บ่อยเกินไป หรือใหญ่เกินไปจะทำให้เกิด connection timeouts ได้. ในตอนนี้ getProgramAccounts
endpoint ยังไม่สนับสนุน pagination. ถ้าผลการค้นหาใหญ่เกินไปผลลัพท์จะถูกตัดทิ้ง.
เพื่อหลีกหนีข้อจำกัดนี้, getProgramAccounts
เลยมี parameters ให้ใช้: ชื่อ, dataSlice
และ filters
options memcmp
และ dataSize
. ถ้าใช้ parameters เหล่านี้, เราจะสามารถลดขอบเขตของการค้นหาให้แคบลงเพื่อควบคุม และประมาณขนาดของผลลัพท์ได้.
ตัวอย่างทั่วไปของ getProgramAccounts
ที่เกี่ยวข้องกับ SPL-Token Program เช่น การค้นหาทุกๆ accounts ที่ Token Program เป็นเจ้าของโดยใช้ การค้นหาแบบปกติ จะทำให้ต้องไปค้นหาข้อมูลมากมาย แต่ถ้าเราใส่ parameters เข้าไปด้วยเราจะสามารถ request ได้อย่างประสิทธิภาพ และได้ data เฉพาะที่เราจะใช้.
filters
parameter ที่ใช้บ่อยๆ สำหรับ getProgramAccounts
คือ filters
array. ซึ่ง array นี้จะรับ filters 2 แบบคือ dataSize
และ memcmp
ก่อนที่จะใช้ filters นี้เราต้องรู้ก่อนว่า data ที่เราจะร้องขอมีรูปแบบยังไง และจัดเรียงไว้ยังไง.
dataSize
ในกรณีของ Token Program, เราจะเห็นว่า token accounts มีขนาด 165 bytes. และ token account จะมี 8 fields ที่แตกต่างกันโดยแต่ละ field จะมีขนาด bytes ที่แน่นอน เราสามารถแสดง visualize ว่า data มีการวางรูปแบบยังไงโดยใช้รูปด้านล่าง.
ถ้าเราต้องการหาทุกๆ 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 ของเรา
เราสามารถลอง 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 datalength
: จำนวนของ 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
) เราก็จะสามารถจำกัดการค้นหาให้มีประสิทธิภาพ และส่งค่ากลับมาเฉพาะที่เราต้องการได้