Obtener cuentas de programa

Existe un método RPC que devuelve todas las cuentas que son propiedad de un programa. Actualmente no se admite la paginación. Las solicitudes a getProgramAccounts deben incluir los parámetros dataSlice y/o filters para mejorar el tiempo de respuesta y devolver solo los resultados que se necesitan.

Hechos

Parámetros

  • programId: string - Llave pública del programa a consultar, codificada en base58
  • (optional) configOrCommitment: object - Parámetros de configuración que contienen los siguientes campos opcionales:
    • (optional) commitment: string - Compromiso del estado (state commitment)open in new window
    • (optional) encoding: string - La codificación para los datos de la cuenta, puede ser: base58, base64, or jsonParsed. Los usuarios de web3js deben usar getParsedProgramAccountsopen in new window
    • (optional) dataSlice: object - Configuración para limitar los datos que se retornan:
      • offset: number - Número de bytes en los datos de la cuenta donde iniciar
      • length: number - Número de bytes de datos de la cuenta a devolver
    • (optional) filters: array - Configuración para filtrar los resultados:
      • memcmp: object - Coincidencia de bytes con los datos de la cuenta:
        • offset: number - Número de bytes en los datos de la cuenta donde empezar a comparar
        • bytes: string - Datos a comparar, como cadena codificada en base58 limitada a 129 bytes
      • dataSize: number - Compara la longitud de los datos de la cuenta con el tamaño de datos proporcionado
    • (optional) withContext: boolean - Envuelve el resultado en un Objeto JSON RpcResponseopen in new window
Respuesta

Por defecto getProgramAccounts devolverá una matriz de objetos JSON con la siguiente estructura:

  • pubkey: string - La clave pública de la cuenta codificada en base58
  • account: object - un objeto JSON, con los siguientes subcampos:
    • lamports: number - número de lamports asignado a la cuenta
    • owner: string - La clave pública del programa al que se ha asignado la cuenta codificada en base58
    • data: string | object - datos asociados con la cuenta, ya sea como datos binarios o en formato JSON según el parámetro de codificación proporcionado
    • executable: boolean - Indicación si la cuenta contiene un programa
    • rentEpoch: number - La época en la que esta cuenta adeudará alquiler

Un vistazo más profundo

getProgramAccounts es un método RPC versátil que devuelve todas las cuentas propiedad de un programa. Podemos usar getProgramAccounts para una serie de consultas útiles, como encontrar:

  • Todas las cuentas de token para una billetera en particular
  • Todas las cuentas para un mint en particular (ej. Todos los titulares (holders) de SRMopen in new window)
  • Todas las cuentas para un programa en particular (ej. Todos los usuarios de Mangoopen in new window)

A pesar de su utilidad, getProgramAccounts a menudo se malinterpreta debido a sus limitaciones actuales. Muchas de las consultas admitidas por getProgramAccounts requieren nodos RPC para escanear grandes conjuntos de datos. Estos escaneos consumen muchos recursos y memoria. Como resultado, las llamadas que son demasiado frecuentes o de un alcance demasiado grande pueden provocar tiempos de espera mayores al permitido (timeouts). Además, en el momento de escribir este artículo, el extremo getProgramAccounts no admite la paginación. Si los resultados de una consulta son demasiado grandes, la respuesta se truncará.

Para resolver temporalmente estas restricciones, getProgramAccounts ofrece una serie de parámetros útiles: por ejemplo, dataSlice y las opciones de filtros memcmp y dataSize. Al proporcionar combinaciones de estos parámetros, podemos reducir el alcance de nuestras consultas a tamaños manejables y predecibles.

Un ejemplo común de getProgramAccounts consiste en interactuar con el Programa de tokens SPLopen in new window. Solicitar todas las cuentas propiedad del Programa Token con una llamada básica implicaría una enorme cantidad de datos. Sin embargo, al proporcionar parámetros, podemos solicitar de manera eficiente solo los datos que pretendemos utilizar.

filters

El parámetro más común para usar con getProgramAccounts es la matriz filters. Esta matriz acepta dos tipos de filtros, dataSize y memcmp. Antes de usar cualquiera de estos filtros, debemos estar familiarizados con la forma en que se distribuyen y serializan los datos que solicitamos.

dataSize

En el caso del Programa Token (Token Program), podemos ver que las cuentas de token tienen una longitud de 165 bytesopen in new window. Específicamente, una cuenta de token tiene ocho campos diferentes, y cada campo requiere una cantidad predecible de bytes. Podemos visualizar cómo se distribuyen estos datos usando la siguiente ilustración.

Tamaño de la cuenta

Si quisiéramos encontrar todas las cuentas de token que pertenecen a nuestra dirección de billetera, podríamos agregar { dataSize: 165 } a nuestra matriz de filtros para limitar el alcance de nuestra consulta a solo cuentas que tengan exactamente 165 bytes de longitud. Esto solo, sin embargo, sería insuficiente. También necesitaríamos agregar un filtro que busque cuentas propiedad de nuestra dirección. Podemos lograr esto con el filtro memcmp.

memcmp

El filtro memcmp, o filtro de "comparación de memoria", nos permite comparar datos en cualquier campo almacenado en nuestra cuenta. Específicamente, solo podemos consultar cuentas que coincidan con un conjunto de bytes en una posición específica. memcmp requiere dos argumentos:

  • offset: La posición para comenzar a comparar datos. Esta posición se mide en bytes y se expresa como un número entero.
  • bytes: Los datos que deben coincidir con los datos de la cuenta. Esto se representa como una cadena codificada en base 58 que debe limitarse a menos de 129 bytes.

Es importante tener en cuenta que memcmp solo devolverá resultados que coincidan exactamente en bytes. Actualmente, no admite comparaciones de valores menores o mayores que los "bytes" que proporcionamos.

De acuerdo con nuestro ejemplo del Programa de tokens, podemos modificar nuestra consulta para que solo devuelva las cuentas de tokens que pertenecen a nuestra dirección de billetera. Al observar una cuenta de token, podemos ver que los dos primeros campos almacenados en una cuenta de token son claves públicas y que cada clave pública tiene una longitud de 32 bytes. Dado que owner es el segundo campo, deberíamos comenzar nuestro memcmp en un offset de 32 bytes. A partir de aquí, buscaremos cuentas cuyo campo de propietario coincida con la dirección de nuestra billetera.

Tamaño de la cuenta

Podemos hacer esta búsqueda utilizando el siguiente ejemplo:

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

Además de los dos parámetros de filtro, el tercer parámetro más común para getProgramAccounts es dataSlice. A diferencia del parámetro filters, dataSlice no reducirá el número de cuentas devueltas por una consulta. En cambio, dataSlice limitará la cantidad de datos para cada cuenta.

Así como memcmp, dataSlice acepta dos argumentos:

  • offset: La posición (en número de bytes) en la que comenzar a devolver los datos de la cuenta
  • length: El número de bytes que se deben devolver

dataSlice es particularmente útil cuando ejecutamos consultas en un gran conjunto de datos pero en realidad no nos preocupamos por los datos de la cuenta en sí. Un ejemplo de esto sería si quisiéramos encontrar la cantidad de cuentas de token (es decir, la cantidad de titulares o holders) para un mint específico.

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
# }

Al combinar los tres parámetros (dataSlice, dataSize y memcmp) podemos limitar el alcance de nuestra consulta y devolver de manera eficiente solo los datos que nos interesan.

Otros recursos

Last Updated:
Contributors: Marco Ordonez