Mendapatkan Akun Program (getProgramAccounts)
adalah sebuah metode RPC untuk mendapatkan semua akun yang dimiliki oleh suatu program. Saat ini pagination tidak didukung. Request ke getProgramAccounts
harus menyertakan parameter dataSlice
dan/atau filters
untuk mempercepat waktu respon dan hanya mengembalikan hasil yang diinginkan.
Fakta
Parameter
programId
:string
- Pubkey dari program yang akan diambil, disediakan sebagai string yang di encode menggunakan base58- (opsional)
configOrCommitment
:object
- Parameter konfigurasi yang berisi field opsional berikut:- (opsional)
commitment
:string
- State commitment - (opsional)
encoding
:string
- Encoding yang digunakan untuk data akun, baik:base58
,base64
, ataujsonParsed
. Catatan, pengguna web3js sebaiknya menggunakan getParsedProgramAccounts - (opsional)
dataSlice
:object
- Membatasi jumlah data akun yang dikembalikan berdasarkan:offset
:number
- Jumlah byte ke dalam data akun untuk mulai kembalilength
:number
- Jumlah byte data akun yang akan dikembalikan
- (opsional)
filters
:array
- Filter hasil menggunakan objek filter berikut:memcmp
:object
- Untuk mencocokkan serangkaian byte dengan data akun:offset
:number
- Posisi byte dalam data akun tempat dimulai perbandingannyabytes
:string
- Data yang sedang dicocokkan berupa string yang di encode base58, dibatasi hingga 129 byte
dataSize
:number
- Membandingkan panjang data akun dengan ukuran data yang disediakan
- (opsional)
withContext
:boolean
- Untuk membungkus hasilnya dalam object RpcResponse JSON
- (opsional)
Response
Secara default getProgramAccounts
akan mengembalikan array dari objek JSON dengan struktur berikut:
pubkey
:string
- Pubkey akun berupa string yang diencode base58account
:object
- sebuah objek JSON, dengan sub-field berikut:lamports
:number
, jumlah lamport yang dimiliki sebuah akunowner
:string
, Pubkey dengan encode base58 dari program tempat akun tersebut dipasangkandata
:string
|object
- data yang terkait dengan akun, baik berupa data biner yang telah diencode atau format JSON tergantung pada parameter jenis encoding yang diberikanexecutable
:boolean
, untuk mengindikasi jika akun tersebut berisi sebuah programrentEpoch
:number
, Epoch di mana akun ini selanjutnya akan berutang sewa
Memahami lebih dalam
getProgramAccounts
adalah metode RPC serbaguna yang mendapatkan semua akun yang dimiliki oleh suatu program. Kita dapat menggunakan getProgramAccounts
untuk sejumlah query yang berguna, seperti menemukan:
- Semua akun token untuk wallet tertentu
- Semua akun token untuk mint tertentu (yaitu Semua pemegang SRM)
- Semua akun khusus untuk program tertentu (yaitu Semua pengguna Mango)
Terlepas dari kegunaannya, getProgramAccounts
sering disalahpahami karena batasannya saat ini. Banyak query yang didukung oleh getProgramAccounts
memerlukan node RPC untuk melakukan scan dari kumpulan data yang besar. Proses scan ini membutuhkan memori dan sumber daya yang intensif. Akibatnya, pemanggilan yang terlalu sering atau terlalu besar cakupannya dapat mengakibatkan connection timeout. Selanjutnya, pada saat penulisan ini, endpoint dari getProgramAccounts
tidak mendukung pagination. Jika hasil query terlalu besar, respons akan dipecah (truncate).
Untuk mengatasi kendala saat ini, getProgramAccounts
menawarkan sejumlah parameter yang berguna: yaitu, dataSlice
dan opsi dari filters
yaitu memcmp
dan dataSize
. Dengan memberikan kombinasi parameter ini, kita dapat mengurangi cakupan query kita menjadi ukuran yang dapat dikelola dan diprediksi.
Contoh umum dari getProgramAccounts
melibatkan interaksi dengan Program Token SPL. Meminta semua akun yang dimiliki oleh Program Token dengan sebuah basic call akan melibatkan sejumlah data yang besar. Namun, dengan memberikan parameter, kita dapat meminta hanya data yang ingin kita gunakan secara efisien.
filters
Parameter yang paling umum digunakan dengan getProgramAccounts
adalah array dari filters
. Array ini menerima dua jenis filter, yaitu dataSize
dan memcmp
. Sebelum menggunakan salah satu dari filter ini, kita harus terbiasa dengan bagaimana data yang kita minta ditata dan diserialisasi.
dataSize
Dalam kasus Program Token, kita dapat melihat bahwa akun token memiliki panjang 165 byte. Secara khusus, akun token memiliki delapan field yang berbeda, dengan masing-masing field membutuhkan jumlah byte yang dapat diprediksi. Kita dapat memvisualisasikan bagaimana data ini ditata menggunakan ilustrasi di bawah ini.
Jika kita ingin menemukan semua akun token yang dimiliki oleh address wallet kita, kita dapat menambahkan { dataSize: 165 }
ke dalam array filters
kita untuk memperkecil cakupan query kita menjadi hanya akun yang panjangnya tepat 165 byte. Namun, ini saja tidak akan cukup. kita juga perlu menambahkan filter untuk yang mencari akun yang dimiliki oleh address kita. Kita bisa mendapatkan ini dengan filter memcmp
.
memcmp
Filter memcmp
, atau filter "memory comparison", memungkinkan kita untuk membandingkan data di field mana pun yang tersimpan di akun kita. Secara khusus, kita hanya dapat melakukan query untuk akun yang cocok dengan sekumpulan byte tertentu pada posisi tertentu. memcmp
membutuhkan dua argumen:
offset
: Posisi untuk mulai membandingkan data. Posisi ini diukur dalam byte dan dinyatakan sebagai bilangan bulat.bytes
: Data yang harus cocok dengan data akun. Ini direpresentasikan dengan string yang diencode menggunakan base-58 dan harus berukuran kurang dari 129 byte.
Penting untuk diperhatikan bahwa memcmp
hanya akan mengembalikan hasil yang bagian dari datanya sama persis dengan bytes
. Saat ini, memcmp
tidak mendukung perbandingan untuk data yang kurang dari atau lebih besar dari bytes
yang kita berikan.
Sesuai dengan contoh Program Token kita, kita dapat mengubah query kita untuk hanya mengembalikan akun token yang dimiliki oleh address wallet kita. Saat melihat akun token, kita dapat melihat dua field pertama yang disimpan di akun token adalah pubkey, dan masing-masing pubkey memiliki panjang 32 byte. Mengingat bahwa owner
adalah field kedua, kita harus memulai memcmp
kita pada offset
32 byte. Dari sini, kita akan mencari akun yang field ownernya cocok dengan address wallet kita.
kita dapat memanggil query ini melalui contoh berikut:
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
Di luar dua parameter filter, parameter ketiga yang paling umum untuk getProgramAccounts
adalah dataSlice
. Tidak seperti parameter filters
, dataSlice
tidak akan mengurangi jumlah akun yang dikembalikan oleh query. Sebagai gantinya, dataSlice
akan membatasi jumlah data untuk setiap akun.
Sama seperti memcmp
, dataSlice
menerima dua argumen:
offset
: Posisi (dalam jumlah byte) untuk mulai mengambil data akunlength
: Jumlah byte yang harus dikembalikan
dataSlice
sangat berguna saat kita menjalankan query pada kumpulan data yang besar tetapi sebenarnya tidak peduli dengan data akun itu sendiri. Contohnya adalah jika kita ingin menemukan jumlah akun token (yaitu jumlah pemegang token) untuk token mint tertentu.
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
# }
Dengan menggabungkan ketiga parameter (dataSlice
, dataSize
, dan memcmp
), kita dapat membatasi batasan query kita dan hanya mengambil data yang kita perlukan secara efisien.