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)open in new window
    • (optionnel) encoding: string - L'encodage des données du compte, peut être: base58, base64, ou jsonParsed. Remarque : les utilisateurs de web3js doivent plutôt utiliser getParsedProgramAccountsopen in new window
    • (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 à retourner
      • length: 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 à comparer
        • bytes: 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 RpcResponseopen in new window
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 base58
  • account: object - un objet JSON, avec les sous-champs suivants :
    • lamports: number, nombre de lamports alloués au compte
    • owner: string, La clé publique du programme auquel le compte a été attribué, encodée en base58
    • data: 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 fourni
    • executable: boolean, Indique si le compte contient un programme
    • rentEpoch: 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 SRMopen in new window)
  • Tous les comptes pour un programme en particulier (par exemple, tous les utilisateurs de Mangoopen in new window)

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 SPLopen in new window. 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 bytesopen in new window. 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.

Taille des comptes

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.

Taille des comptes

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 {
          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

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 compte
  • length: 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 {
          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
# }

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.

Autres Ressources

Last Updated: