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) - (optional)
encoding
:string
- La codificación para los datos de la cuenta, puede ser:base58
,base64
, orjsonParsed
. Los usuarios de web3js deben usar getParsedProgramAccounts - (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 iniciarlength
: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 compararbytes
: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 RpcResponse
- (optional)
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 base58account
:object
- un objeto JSON, con los siguientes subcampos:lamports
:number
- número de lamports asignado a la cuentaowner
:string
- La clave pública del programa al que se ha asignado la cuenta codificada en base58data
:string
|object
- datos asociados con la cuenta, ya sea como datos binarios o en formato JSON según el parámetro de codificación proporcionadoexecutable
:boolean
- Indicación si la cuenta contiene un programarentEpoch
: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 SRM)
- Todas las cuentas para un programa en particular (ej. Todos los usuarios de Mango)
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 SPL. 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 bytes. 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.
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.
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 cuentalength
: 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.