El método RPC getTokenAccountsByOwner de Solana devuelve un número limitado de cuentas de tokens por llamada, por lo que una sola solicitud subestima las carteras con muchas cuentas de tokens. El método utiliza un esquema de paginación basado en cursor con los parámetros before y limit, donde before es la pubkey de la última cuenta de la página anterior. Debido a que el cursor no es un ordenamiento estable y las cuentas pueden crearse o cerrarse entre llamadas, un recorrido correcto debe deduplicar por pubkey de cuenta y terminar en una página vacía. Filtrar por programId requiere consultas separadas para los programas SPL Token y Token-2022 para evitar omitir cuentas. Este artículo explica la mecánica, proporciona una implementación reanudable en Node.js y muestra cómo medir la completitud contra tu propio endpoint.
Por qué una sola llamada a getTokenAccountsByOwner es incompleta para carteras grandes
El método JSON-RPC de Solana getTokenAccountsByOwner devuelve las cuentas de tokens que posee una dirección de cartera determinada. Según la referencia del método getTokenAccountsByOwner de Solana, la respuesta está limitada por un máximo de cuentas por llamada del lado del proveedor. Este límite está documentado como variable según el proveedor, y OnFinality no publica un número específico. La consecuencia práctica es que una cartera que posee más cuentas de tokens que el límite recibirá solo la primera página de resultados, perdiendo silenciosamente el resto a menos que el llamador pagine explícitamente.
Una integración ingenua que llama a getTokenAccountsByOwner una vez y asume que la respuesta está completa subestimará las carteras grandes. Esto no es un error del método RPC; es un diseño deliberado para acotar el tamaño de la respuesta y proteger el rendimiento del nodo. El método proporciona los parámetros before y limit específicamente para permitir a los llamadores recorrer el conjunto completo en múltiples solicitudes. Comprender estos parámetros es esencial para cualquier sistema de producción que necesite un inventario completo de cuentas de tokens.
El centro de aprendizaje de OnFinality contiene guías adyacentes sobre lectura de cuentas y paginación de firmas, pero este artículo se centra exclusivamente en la paginación de cuentas de tokens para carteras grandes. Para una visión más amplia de los métodos RPC de Solana, consulta la guía de la API RPC de Solana (RPC Assistant).
- La respuesta está limitada por un máximo del lado del proveedor (documentado / varía según el proveedor).
- Una sola llamada devuelve solo la primera página; el resto se omite silenciosamente.
- Los parámetros before y limit permiten la paginación basada en cursor.
- La completitud requiere un bucle que continúe hasta que se devuelva una página vacía.
Cómo funcionan before y limit como cursor sobre el conjunto de cuentas
El método getTokenAccountsByOwner acepta un objeto de configuración con los parámetros before y limit. El parámetro before no es un desplazamiento; es un cursor que indica al nodo RPC que devuelva las cuentas cuya pubkey se ordena después de la pubkey dada. El parámetro limit especifica el número máximo de cuentas a devolver en la respuesta. Juntos, permiten al llamador recorrer todo el conjunto de cuentas de tokens que posee una cartera en páginas.
El bucle correcto pasa la pubkey de la última cuenta de la página anterior como valor before para la siguiente solicitud. El bucle termina cuando el RPC devuelve un array vacío, no cuando se alcanza un conteo fijo. Esto es importante porque el número total de cuentas de tokens puede cambiar entre llamadas, y un conteo fijo o bien se detendría antes de tiempo o haría un bucle infinito. El enfoque basado en cursor se adapta naturalmente al estado actual del conjunto de cuentas.
La documentación de Solana para getTokenAccountsByOwner especifica que las entradas de la respuesta llevan la pubkey de la cuenta de token y los datos de la cuenta. Los datos de la cuenta se pueden solicitar en diferentes codificaciones, incluidas jsonParsed y base64. Los parámetros de paginación forman parte del objeto de configuración, junto con commitment, encoding y dataSlice. Para una explicación detallada del diseño de la cuenta SPL Token, consulta Lectura de cuentas de Solana, renta y saldos de tokens.
- before es un cursor sobre el conjunto de cuentas, no un desplazamiento.
- Pasa la pubkey de la última cuenta de la página anterior como el siguiente valor before.
- Termina el bucle en una página vacía, no en un conteo fijo.
- El objeto de configuración también acepta commitment, encoding y dataSlice.
El cursor no es un ordenamiento estable: deduplicación por pubkey de cuenta
El RPC ordena las cuentas de tokens por pubkey, pero este ordenamiento no es estable entre llamadas cuando se crean o cierran cuentas entre páginas. Si una cartera acuña una nueva cuenta de token asociada (ATA) a mitad del recorrido, la pubkey de la nueva cuenta puede ordenarse antes del cursor actual, desplazando el límite de la página y provocando que algunas cuentas se omitan o dupliquen. De manera similar, cerrar una cuenta puede hacer que el cursor omita cuentas que antes estaban en la página siguiente.
Debido a esto, un recorrido de nivel de producción debe deduplicar por pubkey de cuenta en lugar de confiar en que las páginas sean disjuntas. El enfoque más seguro es recopilar todas las pubkeys de cuentas en un conjunto y solo agregar cuentas nuevas que no se hayan visto antes. Esto garantiza que, incluso si los límites de página cambian, el resultado final sea un conjunto completo y único de cuentas de tokens. El recorrido también debe estar preparado para manejar el caso en que una cuenta se cierre y ya no aparezca en ninguna página.
Este comportamiento es consistente con el diseño general del RPC de Solana, donde el conjunto de cuentas está activo y puede cambiar entre solicitudes. El artículo Filtros y dataSlice de getProgramAccounts de Solana analiza consideraciones similares para la paginación de cuentas de programa. Para la paginación de firmas, consulta Paginación de getSignaturesForAddress de Solana.
- El RPC ordena las cuentas por pubkey, pero el ordenamiento no es estable entre llamadas.
- Las cuentas nuevas o cerradas entre páginas pueden desplazar el límite de la página.
- Deduplica por pubkey de cuenta para evitar entradas faltantes o duplicadas.
- El recorrido debe tolerar cuentas que desaparecen a mitad del recorrido.
Filtro por mint vs programId: por qué la elección afecta la completitud
El método getTokenAccountsByOwner requiere un filtro que sea un mint o un programId. Si filtras por mint, obtienes todas las cuentas de tokens para ese mint específico que posee la cartera. Si filtras por programId, obtienes todas las cuentas de tokens que posee la cartera para ese programa de tokens. El programa SPL Token y el programa Token-2022 tienen IDs de programa diferentes, por lo que una sola consulta con el programId de SPL Token no devolverá cuentas de Token-2022.
Para lograr una cobertura completa, el llamador debe consultar cada programId por separado. La documentación de Solana sobre tokens explica que Token-2022 es un programa separado con su propio ID de programa y puede incluir extensiones que cambian el diseño de la cuenta. Una cartera que posee tanto cuentas SPL Token como Token-2022 se subestimará si solo se consulta un programId. Por lo tanto, una enumeración completa requiere al menos dos consultas: una para el programa SPL Token y otra para el programa Token-2022.
Filtrar por mint es útil cuando solo te interesa un token específico, pero para un inventario completo de la cartera, el filtrado por programId es más apropiado. Sin embargo, incluso con el filtrado por programId, debes paginar las cuentas de cada programa por separado porque el cursor before está limitado al filtro. La página de la red Solana de OnFinality proporciona información del endpoint para conectarse a Solana RPC.
- El filtro debe ser un mint o un programId.
- SPL Token y Token-2022 tienen IDs de programa diferentes.
- Una sola consulta por programId omite cuentas del otro programa.
- Consulta cada programId por separado y pagina cada conjunto de resultados.
jsonParsed vs base64 sin procesar: cuándo falla el análisis y cómo recurrir
El método getTokenAccountsByOwner admite un parámetro encoding que se puede establecer en jsonParsed o base64. La codificación jsonParsed es conveniente porque devuelve los datos de la cuenta en una estructura JSON legible por humanos, incluidos el mint, el propietario y la cantidad. Sin embargo, la forma analizada depende de que el entorno de ejecución reconozca el diseño de la cuenta. Las cuentas Token-2022 con extensiones pueden no analizarse correctamente, y el RPC puede devolver un error o recurrir a datos sin procesar.
Un lector de producción no debe depender únicamente de jsonParsed. En su lugar, debe solicitar la codificación base64 y decodificar los desplazamientos fijos del diseño de la cuenta SPL Token. La cuenta SPL Token es una estructura de 165 bytes con la cantidad en un desplazamiento fijo. La referencia del método getTokenAccountsByOwner de Solana documenta las opciones de codificación. Para Token-2022, el diseño de la cuenta incluye extensiones, por lo que el desplazamiento base para la cantidad puede seguir siendo el mismo, pero siguen datos adicionales.
Si jsonParsed falla, el llamador puede capturar el error y reintentar con base64. Alternativamente, el llamador puede usar siempre base64 e implementar su propio analizador. Esto es más robusto pero requiere comprender el diseño de la cuenta. La documentación de Solana sobre tokens proporciona detalles sobre las estructuras de cuentas SPL Token y Token-2022.
- jsonParsed es conveniente pero puede fallar en cuentas Token-2022 con extensiones.
- Recurre a base64 y decodifica los desplazamientos fijos del diseño de la cuenta SPL Token.
- La cuenta SPL Token tiene 165 bytes con la cantidad en un desplazamiento fijo.
- Las cuentas Token-2022 pueden tener datos de extensión adicionales.
Uso de dataSlice como control de ancho de banda y sus límites
El parámetro dataSlice permite al llamador solicitar solo una porción de los datos de la cuenta, especificada por offset y length. Esto puede reducir el ancho de banda cuando solo necesitas un campo específico, como la cantidad. Sin embargo, dataSlice tiene límites: solo se aplica a los datos de la cuenta, no a los metadatos como la pubkey de la cuenta. Además, si usas dataSlice, no puedes usar la codificación jsonParsed porque la forma analizada requiere los datos completos de la cuenta.
Para la paginación, dataSlice puede ser útil para reducir el tamaño de cada respuesta, permitiendo más cuentas por página si el límite del proveedor se basa en el tamaño de la respuesta en lugar del conteo de cuentas. Sin embargo, el máximo del proveedor suele ser un conteo, por lo que dataSlice puede no aumentar el número de cuentas por página. Sigue siendo valioso para reducir el ancho de banda cuando solo necesitas el campo de cantidad. El artículo Filtros y dataSlice de getProgramAccounts de Solana cubre dataSlice con más detalle para cuentas de programa.
Al usar dataSlice, debes conocer el offset y la longitud del campo que necesitas. Para la cuenta SPL Token, la cantidad está en el offset 64 y tiene 8 bytes de longitud. Esto está documentado en el código fuente de SPL Token y en la documentación de Solana sobre tokens. Usar dataSlice con codificación base64 es un patrón común para lecturas eficientes de cuentas de tokens.
- dataSlice solicita una porción de los datos de la cuenta por offset y length.
- No se puede combinar con la codificación jsonParsed.
- Para SPL Token, la cantidad está en el offset 64, longitud 8.
- dataSlice reduce el ancho de banda pero puede no aumentar las cuentas por página.
Un recorrido reanudable en Node.js con before, deduplicación y snapshot estable
El siguiente ejemplo de Node.js demuestra un recorrido reanudable que pagina por before, deduplica por pubkey de cuenta y emite un snapshot estable con clave (mint, cuenta de token). Consulta los IDs de programa SPL Token y Token-2022 por separado. El recorrido termina cuando se devuelve una página vacía para ambos programas. El código usa la biblioteca @solana/web3.js, pero la misma lógica se aplica a cualquier cliente JSON-RPC.
La función getTokenAccountsByOwner se llama con un objeto de configuración que incluye el filtro programId, encoding establecido en base64 y los parámetros before y limit. El parámetro before se actualiza a la pubkey de la última cuenta de la página actual. Los resultados se acumulan en un Map con clave por la pubkey de la cuenta de token para garantizar la unicidad. El snapshot final es un array de objetos que contienen el mint y la pubkey de la cuenta de token.
Esta implementación es reanudable porque se puede detener y reiniciar con el último valor before. En un sistema de producción, persistirías el cursor before y el conjunto acumulado en almacenamiento duradero. El servicio API de OnFinality puede proporcionar endpoints RPC confiables para este recorrido. Para consideraciones de precios, consulta Precios de RPC.
const { Connection, PublicKey } = require('@solana/web3.js');
const SPL_TOKEN_PROGRAM_ID = new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
const TOKEN_2022_PROGRAM_ID = new PublicKey('TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb');
async function getAllTokenAccounts(connection, owner, limit = 1000) {
const ownerPubkey = new PublicKey(owner);
const allAccounts = new Map();
for (const programId of [SPL_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID]) {
let before = undefined;
while (true) {
const config = {
programId,
encoding: 'base64',
limit,
};
if (before) config.before = before;
const response = await connection.getTokenAccountsByOwner(ownerPubkey, config);
const accounts = response.value;
if (accounts.length === 0) break;
for (const { pubkey, account } of accounts) {
if (!allAccounts.has(pubkey.toString())) {
allAccounts.set(pubkey.toString(), {
pubkey: pubkey.toString(),
mint: account.data.slice(0, 32).toString('hex'), // simplified; use proper parsing
data: account.data,
});
}
}
before = accounts[accounts.length - 1].pubkey.toString();
}
}
return Array.from(allAccounts.values());
}
// Usage:
// const connection = new Connection('https://your-rpc-endpoint');
// getAllTokenAccounts(connection, 'WalletAddressHere').then(console.log);Medición de la completitud contra tu propio endpoint: una tabla de resultados
Debido a que el máximo del lado del proveedor y las características de rendimiento varían, debes medir la completitud contra tu propio endpoint. La siguiente tabla proporciona una plantilla para registrar tus observaciones. Ejecuta el recorrido con diferentes valores de limit y registra el número total de cuentas de tokens únicas encontradas, el número de páginas y el tiempo empleado. Esto te ayudará a comprender el comportamiento de tu proveedor de RPC específico.
Para realizar la medición, usa una cartera con un número conocido de cuentas de tokens. Puedes verificar el total consultando un explorador de bloques o usando un proveedor de RPC diferente. Registra los resultados en la tabla a continuación. Si el total varía entre ejecuciones, puede indicar que se están creando o cerrando cuentas durante el recorrido, o que el límite del proveedor está causando truncamiento.
La tabla debe completarse con tus propios datos. No confíes en números de referencia de este artículo, ya que serían fabricados. En su lugar, usa este método para verificar el comportamiento de tu endpoint. La página de la red Solana de OnFinality proporciona URLs de endpoint para pruebas.
- Valor de limit: el parámetro limit usado en el recorrido.
- Páginas: número de llamadas RPC realizadas.
- Cuentas únicas: total de cuentas de tokens únicas encontradas.
- Tiempo (ms): tiempo total del recorrido.
- Notas: cualquier error o anomalía observada.
const { Connection, PublicKey } = require('@solana/web3.js');
const SPL_TOKEN_PROGRAM_ID = new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
const TOKEN_2022_PROGRAM_ID = new PublicKey('TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb');
async function measureWalk(connection, owner, limit) {
const ownerPubkey = new PublicKey(owner);
const seen = new Set();
let pages = 0;
const start = Date.now();
for (const programId of [SPL_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID]) {
let before = undefined;
while (true) {
const config = { programId, encoding: 'base64', limit };
if (before) config.before = before;
const response = await connection.getTokenAccountsByOwner(ownerPubkey, config);
const accounts = response.value;
pages++;
if (accounts.length === 0) break;
for (const { pubkey } of accounts) seen.add(pubkey.toString());
before = accounts[accounts.length - 1].pubkey.toString();
}
}
const elapsed = Date.now() - start;
console.log(`limit=${limit} pages=${pages} unique=${seen.size} timeMs=${elapsed}`);
return { limit, pages, unique: seen.size, timeMs: elapsed };
}
// Usage:
// const connection = new Connection('https://your-rpc-endpoint');
// measureWalk(connection, 'WalletAddressHere', 1000).then(console.log);Limitaciones y compensaciones de la paginación de cuentas de tokens basada en cursor
La paginación basada en cursor con before y limit es la única forma confiable de enumerar todas las cuentas de tokens de una cartera grande, pero tiene compensaciones. El recorrido requiere múltiples llamadas RPC, lo que aumenta la latencia y el costo. El cursor no es un ordenamiento estable, por lo que la deduplicación es obligatoria. El recorrido puede omitir cuentas que se crean y cierran entre páginas, aunque esto es poco frecuente. Para obtener un snapshot completo, es posible que debas ejecutar el recorrido varias veces y reconciliar.
Otra limitación es que el cursor before está limitado al filtro. Si consultas por programId, debes paginar cada programa por separado. Si consultas por mint, debes paginar cada mint por separado. Esto puede resultar en muchas llamadas RPC para carteras con muchos tokens diferentes. Una alternativa es usar el método getProgramAccounts con un filtro en el propietario, pero ese método tiene sus propias limitaciones y no está diseñado para la enumeración de cuentas de tokens. Consulta Streaming de cuentas de getProgramAccounts de Solana para un enfoque de streaming.
Finalmente, el máximo del lado del proveedor no lo publican todos los proveedores. Es posible que debas experimentar para encontrar el límite efectivo. Algunos proveedores pueden devolver menos cuentas que el límite si el tamaño de la respuesta es demasiado grande. Siempre verifica la longitud del array devuelto y continúa hasta que se reciba una página vacía.
- Múltiples llamadas RPC aumentan la latencia y el costo.
- La deduplicación es necesaria debido al ordenamiento inestable.
- Las cuentas creadas y cerradas a mitad del recorrido pueden omitirse.
- El cursor before está limitado al filtro, lo que requiere recorridos separados por programa o mint.
Solución de problemas comunes de paginación
Si tu recorrido devuelve menos cuentas de las esperadas, verifica si estás consultando los IDs de programa SPL Token y Token-2022. Un error común es consultar solo el programa SPL Token y omitir las cuentas Token-2022. Otro problema es usar la codificación jsonParsed, que puede fallar en cuentas Token-2022 con extensiones. Cambia a base64 y decodifica manualmente.
Si el recorrido nunca termina, asegúrate de estar actualizando correctamente el parámetro before. El valor before debe ser la pubkey de la última cuenta de la página anterior. Si pasas accidentalmente la pubkey de la primera cuenta, harás un bucle infinito. Además, verifica que no estés usando un enfoque basado en desplazamiento; el parámetro before es un cursor, no un desplazamiento.
Si encuentras limitación de tasa, reduce el parámetro limit o agrega retrasos entre solicitudes. El servicio API de OnFinality ofrece endpoints RPC escalables que pueden manejar altos volúmenes de solicitudes. Para más consejos de solución de problemas, consulta la guía de la API RPC de Solana (RPC Assistant).
- Cuentas Token-2022 faltantes: consulta ambos IDs de programa.
- Fallos de jsonParsed: recurre a base64.
- Bucle infinito: asegúrate de que before sea la pubkey de la última cuenta.
- Limitación de tasa: reduce limit o agrega retrasos.
Próximos pasos: integrar el recorrido en sistemas de producción
Para integrar este recorrido en un sistema de producción, persiste el cursor before y el conjunto acumulado en almacenamiento duradero. Esto permite que el recorrido se reanude después de un fallo o reinicio. Usa una base de datos con una restricción única en la pubkey de la cuenta de token para manejar la deduplicación automáticamente. Programa el recorrido periódicamente para mantener el snapshot actualizado.
Para actualizaciones en tiempo real, considera usar suscripciones WebSocket a cambios de cuenta, pero ten en cuenta que el snapshot inicial aún requiere un recorrido completo. El artículo Streaming de cuentas de getProgramAccounts de Solana analiza el streaming para cuentas de programa, que se puede adaptar para cuentas de tokens. Para la paginación basada en firmas, consulta Paginación de getSignaturesForAddress de Solana.
Finalmente, prueba tu implementación contra múltiples proveedores de RPC para garantizar la completitud. La página de la red Solana de OnFinality proporciona endpoints para pruebas. Para precios y planes, consulta Precios de RPC.
- Persiste el cursor before y el conjunto acumulado para la reanudabilidad.
- Usa una base de datos con una restricción única en la pubkey de la cuenta de token.
- Programa recorridos periódicos para mantener el snapshot actualizado.
- Prueba contra múltiples proveedores de RPC para verificar la completitud.