Logo
Nuevos usuarios de RPC: 35% de descuento el primer mesVer oferta
OnFinality Learn
Guías de red y protocolo14 min de lectura

Solana getProgramAccounts: dataSlice, filtros y paginación segura

Un método determinista y reanudable para enumerar grandes conjuntos de cuentas de programas de Solana con filtros getProgramAccounts, dataSlice y paginación basada en claves.

TL;DR

getProgramAccounts es el método más costoso y más malinterpretado de Solana porque el nodo RPC realiza un escaneo completo de cuentas en lugar de una búsqueda por índice. Los filtros (dataSize y memcmp) reducen el conjunto de resultados pero no lo paginan, y dataSlice recorta los bytes devueltos sin reducir el número de cuentas. Un escaneo determinista debe paginar sobre un campo on-chain estable y monótonamente creciente, registrar el slot de contexto y la última clave vista, y reanudar desde esa clave en lugar de desde un desplazamiento numérico que Solana no expone. Los límites de respuesta del proveedor y la terminación de solicitudes por costo están documentados / varían según el proveedor, no son errores de protocolo. Los programas cuyas cuentas carecen de un campo monótono no pueden paginarse completamente con getProgramAccounts y requieren un producto de indexación del proveedor o un endpoint estilo getProgramAccountsV2 donde se ofrezca.

Por qué getProgramAccounts es un escaneo completo de cuentas

La referencia oficial del método Solana getProgramAccounts describe getProgramAccounts como el método que devuelve todas las cuentas propiedad de un programa, opcionalmente filtradas por criterios dataSize o memcmp. A diferencia de una búsqueda por índice, el nodo recorre su índice de cuentas para el programa y evalúa cada candidata contra los filtros proporcionados. Ese diseño es lo que hace que el método sea potente para el descubrimiento y costoso para producción: el trabajo escala con el número de cuentas propiedad del programa, no con el tamaño del resultado que solicitaste.

Debido a que el escaneo ocurre en el nodo RPC, el modo de fallo práctico rara vez es un error JSON-RPC a nivel de protocolo. Por lo general, es un límite de respuesta del proveedor, un tiempo de espera de la solicitud o una solicitud cancelada por costo. Estos comportamientos están documentados / varían según el proveedor, por lo que la misma llamada puede tener éxito en un endpoint y ser truncada o rechazada en otro. Trata el método como una primitiva de descubrimiento, no como una consulta de alta frecuencia, y diseña tu escáner para que sea reanudable desde la primera solicitud.

El modelo de cuentas en sí se describe en la documentación de cuentas de Solana: cada cuenta tiene un propietario, lamports, indicador de ejecutable, época de renta y un arreglo de bytes de datos. getProgramAccounts devuelve ese arreglo de datos, por lo que los filtros y dataSlice operan sobre bytes en lugar de campos con nombre. Para una orientación más amplia sobre las superficies RPC de Solana, consulta la descripción general de redes Solana y el centro de aprendizaje de OnFinality.

  • El nodo escanea las cuentas propiedad del programa; el costo escala con el número de cuentas del programa.
  • Los límites del proveedor y la terminación por costo están documentados / varían según el proveedor, no son errores JSON-RPC.
  • Los filtros reducen el conjunto; no lo paginan.
  • dataSlice recorta los bytes devueltos; no reduce el número de cuentas devueltas.

El slot de contexto de RpcResponse y por qué cada página debe registrarlo

Una respuesta de getProgramAccounts está envuelta en el sobre estándar RpcResponse, que incluye un objeto context con un slot. Ese slot identifica el banco que el nodo usó para responder la solicitud. Debido a que las cuentas cambian entre solicitudes, dos páginas del mismo escaneo lógico pueden responderse en slots diferentes, y una cuenta creada o cerrada en el intermedio puede aparecer en una página y no en la otra.

Registrar el slot de contexto en cada página no es, por lo tanto, una simple contabilidad; es la única forma de razonar sobre la consistencia a posteriori. Si tu escaneo abarca muchas solicitudes, almacena el slot junto con la última clave que procesaste. Cuando posteriormente concilies el escaneo con una segunda fuente, podrás comparar recuentos aproximadamente en el mismo slot en lugar de a lo largo de una ventana ilimitada.

La opción commitment controla desde qué banco responde el nodo. Un commitment más débil responde más rápido pero puede revertirse; un commitment más fuerte es más estable pero puede retrasarse. La referencia del método documenta commitment como parámetro de solicitud, y el valor predeterminado exacto y los niveles admitidos están documentados / varían según el proveedor. Para una discusión relacionada sobre la consistencia a nivel de slot, consulta Solana getBlocks y slots omitidos.

  • Cada página lleva un slot de contexto; almacénalo con la última clave.
  • Las cuentas pueden cambiar entre páginas, por lo que un escaneo es una secuencia de instantáneas, no una sola instantánea.
  • El commitment afecta qué banco responde; los valores predeterminados y los niveles admitidos varían según el proveedor.

Semántica de filtros: dataSize, memcmp y composición AND

La documentación del modelo de cuentas de Solana define cómo se disponen y poseen los datos de las cuentas, y la referencia del método define dos tipos de filtro. Un filtro dataSize coincide con la longitud del arreglo de datos de la cuenta en bytes. Un filtro memcmp compara una cadena de bytes en un desplazamiento dado dentro de los datos de la cuenta. Ambos se evalúan contra los datos completos de la cuenta en el nodo, antes de aplicar cualquier recorte de dataSlice.

Los filtros se combinan con AND. Agregar un filtro reduce el conjunto de candidatas; nunca lo pagina. Esta es la mala interpretación más común de getProgramAccounts: los desarrolladores agregan un filtro memcmp esperando la siguiente página y en su lugar reciben una primera página más pequeña. Si tu conjunto de filtros es demasiado amplio, obtienes una respuesta grande; si es demasiado estrecho, obtienes una pequeña. Ninguno de los dos resultados es paginación.

Una consecuencia práctica es que el diseño de filtros es un ejercicio de selectividad. Un filtro dataSize es barato y a menudo muy selectivo para cuentas de diseño fijo. Un filtro memcmp sobre un discriminador o un campo conocido es más preciso pero requiere que conozcas el desplazamiento en bytes. Combinar ambos es común: dataSize para seleccionar el tipo de cuenta, memcmp para seleccionar un subconjunto dentro de ese tipo.

  • dataSize coincide con la longitud de los datos de la cuenta en bytes.
  • memcmp compara una cadena de bytes en un desplazamiento dado dentro de los datos de la cuenta.
  • Los filtros se combinan con AND; agregar uno reduce el conjunto en lugar de avanzar un cursor.
  • Los filtros se evalúan contra los datos completos de la cuenta, antes de dataSlice.

Desplazamientos memcmp y el contrato de diseño de la estructura de cuenta

Un desplazamiento memcmp es un desplazamiento en bytes dentro de los datos de la cuenta serializados en bincode. No es un nombre de campo ni un índice lógico. Por lo tanto, el desplazamiento depende del diseño exacto de la estructura de cuenta producido por el programa. Para programas Anchor, los primeros ocho bytes son el discriminador de cuenta, por lo que un campo que aparece primero en la estructura Rust comienza en el desplazamiento 8, no en 0.

Esto hace que cualquier desplazamiento calculado a partir de una definición de estructura sea un contrato versionado. Si el programa luego agrega un campo antes del que filtras, o cambia el tipo de un campo, tu desplazamiento apunta silenciosamente a los bytes incorrectos. El filtro aún se ejecuta; simplemente coincide con los datos equivocados. Trata los desplazamientos como parte de tu superficie de integración y fíjalos a una versión del programa.

La documentación de cuentas de Solana describe los datos de la cuenta como un arreglo de bytes opaco propiedad del programa, por lo que la capa RPC solo puede ofrecer filtros a nivel de bytes. Para una lectura de una sola cuenta que evita este problema por completo, consulta Lectura de información de cuenta y renta en Solana.

  • El desplazamiento memcmp es un desplazamiento en bytes dentro de los datos de la cuenta serializados en bincode.
  • Las cuentas Anchor comienzan con un discriminador de 8 bytes, por lo que el primer campo de la estructura comienza en el desplazamiento 8.
  • Un desplazamiento derivado de una definición de estructura es un contrato versionado que se rompe cuando cambia el diseño.
  • Fija los desplazamientos a una versión del programa y vuelve a verificarlos después de las actualizaciones del programa.

dataSlice como control de ancho de banda, no como mecanismo de paginación

La referencia del método documenta dataSlice como un offset y length opcionales que devuelven solo esa porción de los datos de cada cuenta. Reduce los bytes en la red. No reduce el número de cuentas devueltas. Un escaneo que devuelve diez mil cuentas sigue devolviendo diez mil cuentas con dataSlice; cada una es simplemente más pequeña.

Esta distinción importa porque dataSlice por sí solo nunca hace que un escaneo grande sea barato. El nodo sigue escaneando y sigue serializando una respuesta que contiene todas las cuentas coincidentes. Lo que dataSlice cambia es el tamaño de la carga útil por cuenta, lo que puede llevar una respuesta de superar un límite del proveedor a quedar por debajo de él, pero no puede llevar una respuesta de diez mil cuentas a cien.

También hay una trampa de usabilidad: si recortas el campo que necesitas para ordenar o reanudar, el conjunto de resultados se vuelve inutilizable para la paginación. Elige la porción para cubrir exactamente el campo clave sobre el que pretendes paginar y mantén ese campo en cada respuesta.

  • dataSlice devuelve offset..offset+length de los datos de cada cuenta.
  • Reduce los bytes en la red pero no el número de cuentas devueltas.
  • Recortar tu clave de ordenación o reanudación hace que el conjunto de resultados sea inutilizable.
  • Elige la porción para cubrir el campo clave sobre el que paginas.

Cómo interactúa dataSlice con los filtros

Los filtros se evalúan contra los datos completos de la cuenta en el nodo. dataSlice se aplica solo a la carga útil devuelta. Este orden significa que un filtro sobre un campo que también recortaste es legal: el nodo puede coincidir con bytes que no devolverá. Puedes filtrar por un discriminador en el desplazamiento 0 y devolver solo los bytes 8 a 40, por ejemplo.

La implicación práctica es que puedes mantener las respuestas pequeñas mientras usas filtros precisos. El riesgo es que pierdes la capacidad de verificar la coincidencia localmente, porque los bytes coincidentes no están en la respuesta. Si necesitas auditar la corrección del filtro, amplía temporalmente la porción o ejecuta una consulta de verificación separada.

Esta separación también es la razón por la que dataSlice no puede usarse para implementar paginación. La paginación requiere una clave de ordenación estable en la respuesta; dataSlice solo controla qué bytes de cada cuenta ves. Para un patrón relacionado en un método diferente, consulta Paginación de Solana getSignaturesForAddress.

  • Los filtros se ejecutan contra los datos completos de la cuenta; dataSlice recorta solo la carga útil devuelta.
  • Filtrar por un campo que recortaste es legal.
  • Amplía la porción temporalmente si necesitas auditar la corrección del filtro.
  • dataSlice no puede implementar paginación porque no ordena los resultados.

Diseñar un escaneo reanudable sobre una clave estable

Solana no expone un desplazamiento numérico ni un cursor para getProgramAccounts. Por lo tanto, cualquier esquema de paginación debe construirse sobre un campo dentro de los datos de la cuenta. El patrón viable es paginar sobre un campo on-chain monótonamente creciente, registrar la última clave vista y reanudar desde ella. Cada solicitud filtra las cuentas cuya clave es mayor que la última clave, ordena los resultados localmente y avanza el cursor hasta la clave máxima del lote.

Este es un patrón de paginación por keyset adaptado a un método que no tiene cursor nativo. Es determinista siempre que el campo clave sea único y monótono. Si la clave no es única, necesitas un desempate, y si no es monótona, el escaneo puede omitir o duplicar cuentas. El slot de contexto de cada respuesta te indica de qué instantánea provino el lote.

Para un tratamiento más amplio de la consulta de estado histórico a través de RPC, consulta Consulta de datos históricos de Solana a través de RPC. La misma disciplina de registrar slot y cursor se aplica allí.

  • Solana no expone ningún desplazamiento numérico ni cursor para getProgramAccounts.
  • Pagina sobre un campo on-chain monótonamente creciente y reanuda desde la última clave.
  • Ordena cada lote localmente y avanza el cursor hasta la clave máxima.
  • Registra el slot de contexto con la última clave para una conciliación posterior.

Escáner Node.js ejecutable con dataSize, memcmp y dataSlice

El escáner a continuación emite getProgramAccounts con un filtro dataSize, un filtro memcmp sobre un discriminador y un dataSlice que cubre solo el campo clave. Emite un resumen por lote y puede volver a ejecutarse desde una última clave registrada. Reemplaza el ID del programa, el discriminador y los desplazamientos con los valores de tu programa.

El script usa la forma de solicitud estándar JSON-RPC 2.0 y lee el slot de contexto de cada respuesta. No asume ninguna extensión específica del proveedor. Si tu proveedor ofrece un endpoint estilo getProgramAccountsV2, la misma lógica de cursor se aplica, pero la forma de la solicitud está documentada / varía según el proveedor.

// scan.js — resumable getProgramAccounts scanner
// Usage: node scan.js [lastKeyBase58]
const RPC_URL = process.env.RPC_URL || 'https://api.mainnet-beta.solana.com';
const PROGRAM_ID = process.env.PROGRAM_ID; // your program id
const DATA_SIZE = Number(process.env.DATA_SIZE || 165);
const KEY_OFFSET = Number(process.env.KEY_OFFSET || 8);
const KEY_LENGTH = Number(process.env.KEY_LENGTH || 32);
const DISCRIMINATOR_B58 = process.env.DISCRIMINATOR_B58; // optional

async function rpc(method, params) {
  const res = await fetch(RPC_URL, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }),
  });
  const json = await res.json();
  if (json.error) throw new Error(JSON.stringify(json.error));
  return json.result;
}

async function scan(lastKey) {
  const filters = [{ dataSize: DATA_SIZE }];
  if (DISCRIMINATOR_B58) {
    filters.push({ memcmp: { offset: 0, bytes: DISCRIMINATOR_B58 } });
  }
  const result = await rpc('getProgramAccounts', [
    PROGRAM_ID,
    {
      encoding: 'base64',
      commitment: 'confirmed',
      withContext: true,
      filters,
      dataSlice: { offset: KEY_OFFSET, length: KEY_LENGTH },
    },
  ]);
  const slot = result.context.slot;
  const accounts = result.value;
  const keys = accounts.map((a) => Buffer.from(a.account.data[0], 'base64'));
  keys.sort(Buffer.compare);
  const last = keys.length ? keys[keys.length - 1].toString('hex') : lastKey;
  const bytes = accounts.reduce((n, a) => n + Buffer.from(a.account.data[0], 'base64').length, 0);
  console.log(JSON.stringify({
    filter: filters,
    accountsReturned: accounts.length,
    bytesReturned: bytes,
    contextSlot: slot,
    lastKey: last,
  }));
  return last;
}

scan(process.argv[2]).catch((e) => { console.error(e); process.exit(1); });

Verificar la completitud del escaneo contra una segunda fuente

Un escaneo solo es útil si puedes argumentar que está completo. La verificación cruzada más económica es una segunda fuente que informe el mismo recuento de cuentas: un producto de indexación del proveedor, un endpoint estilo getProgramAccountsV2 donde se ofrezca, o un endpoint RPC separado consultado en un slot cercano. Compara los recuentos e investiga cualquier discrepancia antes de confiar en el escaneo.

Debido a que las cuentas cambian entre solicitudes, la igualdad exacta no siempre es alcanzable. Registra el slot de contexto de cada lote y compara los recuentos aproximadamente en el mismo slot. Si la segunda fuente informa un recuento materialmente diferente, las causas probables son un filtro demasiado estrecho, un cursor que omitió un rango o un límite del proveedor que truncó una respuesta.

Para la selección de endpoints y el comportamiento de conmutación por error, consulta la guía de endpoints RPC (RPC Assistant). Ejecutar el mismo escaneo contra dos endpoints es una forma práctica de detectar truncamiento específico del proveedor.

  • Verifica el recuento de cuentas contra una segunda fuente.
  • Compara los recuentos aproximadamente en el mismo slot de contexto.
  • Investiga las discrepancias antes de confiar en el escaneo.
  • Ejecuta el mismo escaneo contra dos endpoints para detectar truncamiento específico del proveedor.

Tabla de resultados: medir tu propio endpoint

El comportamiento del proveedor está documentado / varía según el proveedor, por lo que los únicos números confiables son los que mides contra tu propio endpoint. Ejecuta el escáner con un conjunto de filtros fijo y registra las siguientes columnas para cada lote. No compares tus números con cifras publicadas; compáralos con tu propia línea base a lo largo del tiempo.

La tabla a continuación es una plantilla. Rellénala con valores de tu endpoint y consérvala junto con la salida del escaneo. Si un lote devuelve cero cuentas o una carga útil truncada, anota el slot de contexto y el filtro utilizado para que puedas reproducir la condición.

  • Índice del lote
  • Filtro utilizado (dataSize, memcmp offset/bytes)
  • Cuentas devueltas
  • Bytes devueltos
  • Slot de contexto
  • Última clave
  • Duración en tiempo real
  • Estado de respuesta del proveedor o error

Limitaciones y compensaciones de la paginación con getProgramAccounts

La limitación honesta es que un programa cuyas cuentas no tienen un campo monótono no puede paginarse completamente con getProgramAccounts en absoluto. Sin una clave de ordenación estable, no hay cursor desde el cual reanudar, y cualquier esquema similar a un desplazamiento omitirá o duplicará cuentas a medida que cambie el conjunto. En ese caso, el lector debe usar un producto de indexación del proveedor o un endpoint estilo getProgramAccountsV2 donde el proveedor ofrezca uno, lo cual está documentado / varía según el proveedor.

Incluso con una clave monótona, el escaneo es una secuencia de instantáneas en lugar de una única vista consistente. Las cuentas creadas o cerradas entre lotes pueden omitirse o contarse dos veces. El slot de contexto te permite razonar sobre esto a posteriori, pero no lo elimina. Si necesitas una instantánea consistente, necesitas un producto de indexación que la mantenga.

Finalmente, el costo y los límites de velocidad son restricciones reales. Un escaneo completo es costoso en el nodo, y los proveedores pueden limitar el tamaño de la respuesta o terminar solicitudes por costo. Diseña tu escáner para que sea reanudable, para que registre su cursor y para que tolere lotes parciales. Para precios y contexto del servicio, consulta Precios de RPC y el servicio de API.

  • Sin un campo monótono no hay paginación completa con getProgramAccounts.
  • Un escaneo es una secuencia de instantáneas, no una vista consistente.
  • Los límites del proveedor y la terminación por costo están documentados / varían según el proveedor.
  • Diseña para la reanudabilidad y los lotes parciales.

Solución de problemas comunes de getProgramAccounts

El fallo más común es una respuesta más pequeña de lo esperado. Verifica si un filtro es demasiado estrecho, si el desplazamiento memcmp apunta a los bytes incorrectos después de una actualización del programa, o si el proveedor truncó la respuesta. El slot de contexto y el conjunto de filtros en el resumen de tu lote son lo primero que debes inspeccionar.

El segundo fallo común es una solicitud que se rechaza o se cancela. Por lo general, es un límite del proveedor o un control de costos, no un error de protocolo JSON-RPC. Reduce el conjunto de resultados con un filtro más selectivo, encoge el dataSlice o cambia a un endpoint con límites diferentes. El comportamiento está documentado / varía según el proveedor.

El tercer fallo común es un escaneo que parece repetirse o saltarse elementos. Esto generalmente significa que la clave del cursor no es única o no es monótona. Agrega un desempate, verifica que el campo clave realmente esté aumentando y confirma que el dataSlice todavía incluya el campo clave. Para un patrón de paginación relacionado, consulta Paginación de Solana getSignaturesForAddress.

  • Respuesta más pequeña de lo esperado: verifica la selectividad del filtro, el desplazamiento memcmp y el truncamiento del proveedor.
  • Solicitud rechazada o cancelada: límite del proveedor o control de costos, documentado / varía según el proveedor.
  • Escaneo que se repite o se salta elementos: la clave del cursor no es única o no es monótona.
  • Confirma que el dataSlice todavía incluya el campo clave.

Próximos pasos para la enumeración de cuentas en producción

Si tu programa tiene una clave monótona, el escáner de este artículo es un punto de partida viable. Agrega persistencia para la última clave y el slot de contexto, programa el escaneo y verifica los recuentos contra una segunda fuente. Si tu programa carece de una clave monótona, evalúa un producto de indexación del proveedor o un endpoint estilo getProgramAccountsV2 antes de construir una solución alternativa personalizada.

Para la selección de endpoints y la conmutación por error, revisa la guía de endpoints RPC (RPC Assistant). Para una cobertura más amplia de RPC de Solana, consulta la descripción general de redes Solana y el centro de aprendizaje de OnFinality. Para detalles de precios y servicio, consulta Precios de RPC y el servicio de API.

Las fuentes primarias autorizadas para la semántica descrita aquí son la referencia del método Solana getProgramAccounts en https://solana.com/docs/rpc/http/getprogramaccounts y la documentación de cuentas de Solana en https://solana.com/docs/core/accounts. Los límites y extensiones específicos del proveedor siempre deben confirmarse con la documentación actual de tu proveedor.

  • Persiste la última clave y el slot de contexto; programa y verifica el escaneo.
  • Evalúa productos de indexación si no existe una clave monótona.
  • Confirma los límites y extensiones del proveedor con la documentación actual.

Nunca te preocupes por la infraestructura nuevamente

OnFinality elimina la carga pesada de DevOps para que puedas construir de forma más inteligente y rápida.

Comenzar