Obtenir les comptes d'un programme
Il existe une méthode RPC qui renvoie tous les comptes appartenant à un programme. Actuellement, la pagination n'est pas prise en charge. Les requêtes à getProgramAccounts
devraient inclure les paramètres dataSlice
et/ou filters
afin d'améliorer le temps de réponse et retourner uniquement les résultats voulus.
Faits
Paramètres
programId
:string
- Clé publique du programme à interroger, fournie sous forme de chaîne de caractères codée en base58- (optionnel)
configOrCommitment
:object
- Paramètres de configuration contenant les champs facultatifs suivants :- (optionnel)
commitment
:string
- Engagement de l'État (State commitment) - (optionnel)
encoding
:string
- L'encodage des données du compte, peut être:base58
,base64
, oujsonParsed
. Remarque : les utilisateurs de web3js doivent plutôt utiliser getParsedProgramAccounts - (optionnel)
dataSlice
:object
- Paramètres permettant de limiter les données à renvoyer :offset
:number
- Nombre de bytes dans les données du compte à partir desquels il faut commencer à retournerlength
:number
- Nombre de bytes de données du compte à retourner
- (optionnel)
filters
:array
- Paramètres pour filtrer les résultats :memcmp
:object
- Correspondance d'une série de bytes avec les données du compte :offset
:number
- Nombre de bytes dans les données du compte à partir desquels il faut commencer à comparerbytes
:string
- Données à comparer, sous la forme d'une chaîne de caractères codée en base58 limitée à 129 bytes
dataSize
:number
- Compare la longueur des données du compte avec la taille des données fournies
- (optionnel)
withContext
:boolean
- Enveloppe le résultat dans un objet JSON RpcResponse
- (optionnel)
Réponse
Par défaut, getProgramAccounts
retournera un tableau d'objets JSON avec la structure suivante :
pubkey
:string
- La clé publique du compte sous la forme d'une chaîne de caractères encodée en base58account
:object
- un objet JSON, avec les sous-champs suivants :lamports
:number
, nombre de lamports alloués au compteowner
:string
, La clé publique du programme auquel le compte a été attribué, encodée en base58data
:string
|object
- les données associées au compte, soit sous forme de données binaires, soit au format JSON, conformément au paramètre d'encodage fourniexecutable
:boolean
, Indique si le compte contient un programmerentEpoch
:number
, L'époque à laquelle ce compte devra payer sa prochaine rente
Examen plus approfondi
getProgramAccounts
est une méthode RPC polyvalente qui renvoie tous les comptes appartenant à un programme. Nous pouvons utiliser getProgramAccounts
pour un certain nombre de requêtes utiles, telles que la recherche de :
- Tous les comptes de jetons pour un portefeuille en particulier
- Tous les comptes de jetons pour un mint en particulier (par exemple, tous les propriétaires (holders) de SRM)
- Tous les comptes pour un programme en particulier (par exemple, tous les utilisateurs de Mango)
Malgré son utilité, getProgramAccounts
est souvent mal compris en raison de ses limites actuelles. La plupart des requêtes supportées par getProgramAccounts
nécessitent des nœuds RPC pour analyser de grands ensembles de données. Ces analyses sont à la fois gourmandes en mémoire et en ressources. Par conséquent, les appels trop fréquents ou de trop grande envergure peuvent entraîner des interruptions de connexion. De plus, au moment où nous écrivons ces lignes, le point de terminaison getProgramAccounts
ne prend pas en charge la pagination. Si les résultats d'une requête sont trop volumineux, la réponse sera tronquée.
Pour contourner ces contraintes actuelles, getProgramAccounts
offre un certain nombre de paramètres utiles : à savoir, dataSlice
et les options de filters
memcmp
et dataSize
. En fournissant des combinaisons de ces paramètres, nous pouvons réduire la portée de nos requêtes à des tailles gérables et prévisibles.
Un exemple courant de getProgramAccounts
consiste à interagir avec le Programme de Jetons SPL. Demander tous les comptes détenus par le programme de Jetons avec un appel de base impliquerait une énorme quantité de données. Cependant, en fournissant des paramètres, nous pouvons efficacement demander uniquement les données que nous avons l'intention d'utiliser.
filters
Le paramètre le plus commun à utiliser avec getProgramAccounts
est le tableau filters
. Ce tableau accepte deux types de filtres, dataSize
et memcmp
. Avant d'utiliser l'un de ces filtres, nous devons nous familiariser avec la manière dont les données que nous demandons sont organisées et sérialisées.
dataSize
Dans le cas du Programme de Jetons, nous pouvons constater que les comptes de jetons ont une taille de 165 bytes. Plus précisément, un compte de jeton comporte huit champs différents, chaque champ ayant un nombre prédéfini de bytes. Nous pouvons visualiser comment ces données sont organisées à l'aide de l'illustration ci-dessous.
Si nous voulions trouver tous les comptes de jetons appartenant à notre adresse de portefeuille, nous pourrions ajouter { dataSize: 165 }
à notre tableau filters
pour limiter notre requête aux seuls comptes qui font exactement 165 bytes de long. Toutefois, cela ne suffirait pas. Nous devrions également ajouter un filtre qui recherche les comptes appartenant à notre adresse. Nous pouvons réaliser cela avec le filtre memcmp
.
memcmp
Le filtre memcmp
, ou filtre "comparaison de mémoire", nous permet de comparer des données à n'importe quel champ stocké sur notre compte. Plus précisément, nous pouvons rechercher uniquement les comptes qui possèdent un certain ensemble de bytes à une position précise. memcmp
nécessite deux arguments :
offset
: La position à partir de laquelle il faut commencer à comparer les données. Cette position est mesurée en bytes et est exprimée sous la forme d'un nombre entier.bytes
: Les données qui doivent correspondre aux données du compte. Elles sont représentées sous la forme d'une chaîne de caractères codées en base 58 qui doit être limitée à moins de 129 bytes.
Il est important de noter que memcmp
ne retournera que les résultats qui correspondent exactement aux bytes
. Actuellement, il ne supporte pas les comparaisons pour les valeurs inférieures ou supérieures aux bytes
que nous fournissons.
Pour rester dans notre exemple du Programme de Jetons, nous pouvons modifier notre requête pour ne renvoyer que les comptes de jetons qui appartiennent à notre adresse de portefeuille. En examinant un compte de jetons, on constate que les deux premiers champs stockés sur un compte de jetons sont tous deux des clés publiques, et que chaque clé publique a une longueur de 32 octets. Étant donné que owner
est le deuxième champ, nous devrions commencer notre memcmp
à un offset
de 32 bytes. A partir de là, nous allons rechercher les comptes dont le champ propriétaire (owner) correspond à l'adresse de notre portefeuille.
Nous pouvons faire appel à cette requête via l'exemple suivant :
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
En plus des deux paramètres de filtre, le troisième paramètre le plus courant pour getProgramAccounts
est dataSlice
. Contrairement au paramètre filters
, dataSlice
ne réduira pas le nombre de comptes retournés par une requête. Au lieu de cela, dataSlice
limitera la quantité de données pour chaque compte.
Tout comme memcmp
, dataSlice
accepte deux arguments :
offset
: La position (en nombre de bytes) à partir de laquelle il faut commencer à renvoyer les données du comptelength
: Le nombre d'octets qui doivent être retournés
dataSlice
est particulièrement utile lorsque nous exécutons des requêtes sur un grand ensemble de données mais que nous ne nous soucions pas vraiment des données du compte lui-même. Par exemple, si nous voulons trouver le nombre de comptes de jetons (c'est-à-dire le nombre de détenteurs de jetons) pour un jeton en particulier.
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
# }
En combinant ces trois paramètres (dataSlice
, dataSize
, et memcmp
), nous pouvons limiter la portée de notre requête et ne renvoyer efficacement que les données qui nous intéressent.