Solana accountSubscribe acepta un parámetro encoding opcional con tres valores documentados: base64, base64+zstd y jsonParsed. El sobre de la notificación (context y value) es idéntico entre codificaciones; solo cambia la forma de value.data. base64 devuelve una tupla de dos elementos [data, encoding] que contiene los bytes sin procesar de la cuenta, mientras que jsonParsed devuelve un objeto con los campos program y parsed producidos por el parser del programa propietario. jsonParsed no es universal: las cuentas cuyo programa propietario no tiene un parser en el runtime devuelven parsed: null, por lo que los clientes que solo implementan la rama jsonParsed descartan silenciosamente esas actualizaciones. Los indexadores en producción suelen solicitar base64 por estabilidad y usar jsonParsed para inspección, porque la salida del parser puede cambiar con las versiones del nodo. Este artículo muestra ejemplos ejecutables en Node.js para ambas codificaciones, una tabla de resultados para verificar contra tu propio endpoint y solución de problemas para discrepancias de forma, payloads comprimidos y correlación de IDs de suscripción.
La solicitud accountSubscribe y su parámetro encoding
accountSubscribe es un método WebSocket de Solana que registra una suscripción a los cambios de una única cuenta identificada por su clave pública en base58. La solicitud es una llamada JSON-RPC 2.0 estándar sobre una conexión WebSocket, que lleva un id, el nombre del método y un array params. La documentación de Solana para accountSubscribe especifica los params como la pubkey de la cuenta, un nivel de commitment opcional y un valor de encoding opcional. Los valores de encoding documentados son base64, base64+zstd y jsonParsed.
El parámetro encoding controla únicamente cómo se representa el campo data de la cuenta en las notificaciones. No cambia qué cuenta se observa, cuándo se disparan las notificaciones ni el sobre de la notificación circundante. Si omites encoding, el nodo aplica su valor por defecto, que en el comportamiento documentado es base64. Como la elección es por suscripción, puedes abrir dos suscripciones a la misma cuenta con codificaciones diferentes y compararlas en paralelo, que es la forma más rápida de interiorizar la diferencia.
La suscripción se confirma con una respuesta que contiene el id de suscripción, y las notificaciones posteriores llegan como notificaciones JSON-RPC con el método accountNotification. Ese id es lo que usas para correlacionar las notificaciones con la suscripción que las produjo, un tema tratado en Correlación de IDs de notificaciones JSON-RPC y orden de lotes.
- Params: pubkey de la cuenta (cadena base58), commitment opcional, encoding opcional.
- Codificaciones documentadas: base64, base64+zstd, jsonParsed.
- Método de notificación: accountNotification, con un id de suscripción para correlacionar.
Qué devuelve base64: bytes sin procesar en una tupla
Con encoding base64, el value.data de la notificación es un array de dos elementos: el primer elemento son los bytes sin procesar de la cuenta codificados como una cadena base64, y el segundo elemento es la cadena literal "base64". Esta forma de tupla está documentada en la página de estructuras JSON de RPC de Solana, que describe las representaciones de datos de cuentas. Los bytes son sin pérdida: lo que el programa escribió en la cuenta es exactamente lo que recibes, sin interpretación aplicada por el nodo.
Como el nodo no realiza ningún parseo, base64 es la opción estable para los indexadores en producción. Tu cliente es dueño del paso de deserialización, lo que significa que debes mantener un layout Borsh sincronizado con el programa on-chain. Eso es trabajo real, pero es trabajo que tú controlas. Una actualización del nodo no puede cambiar el significado de tus bytes, porque el nodo nunca les asignó un significado en primer lugar.
La forma de tupla es fácil de manejar mal. Un error común es tratar value.data como una cadena y llamar directamente a Buffer.from(data, 'base64'), lo que falla porque data es un array. El acceso correcto es data[0] para la cadena base64 y data[1] para la etiqueta de codificación. Ramificar según data[1] en lugar de asumir base64 es un hábito defensivo barato si tu ruta de código también podría recibir jsonParsed.
- value.data es [cadenaBase64, "base64"].
- Sin pérdida: sin interpretación del lado del nodo.
- El cliente debe deserializar con un layout mantenido en sincronía con el programa.
Qué devuelve base64+zstd y cuándo ayuda la compresión
base64+zstd aplica compresión zstd a los mismos bytes sin procesar antes de codificarlos en base64, por lo que el payload es más pequeño en el cable. La etiqueta de codificación documentada en la tupla es "base64+zstd". Esto es más útil para cuentas grandes donde el ancho de banda o el tamaño del mensaje son una restricción, como cuentas que contienen estado o arrays considerables.
La contrapartida es que tu cliente debe descomprimir antes de poder deserializar. Node.js no incluye un descompresor zstd en la biblioteca estándar, por lo que necesitarás una dependencia o un binding nativo. Eso añade una pieza móvil a tu pipeline. Para cuentas pequeñas, el overhead de compresión puede no valer la dependencia añadida, y el lector debería medir en lugar de asumir.
La compresión cambia la representación en el cable, no la semántica. Una vez descomprimidos, los bytes son idénticos a los que habría entregado base64. Si estás depurando una discrepancia de forma, confirma qué etiqueta de codificación recibiste realmente antes de escribir lógica de descompresión, porque una suscripción base64 nunca producirá un payload zstd.
- Los mismos bytes que base64, comprimidos con zstd y luego codificados en base64.
- La etiqueta de la tupla es "base64+zstd".
- Requiere un descompresor zstd en el cliente; mide el beneficio según el tamaño de la cuenta.
Qué devuelve jsonParsed: salida del parser y sus límites
Con encoding jsonParsed, el nodo pasa la cuenta por el parser asociado a su programa propietario y devuelve value.data como un objeto con los campos program y parsed. El campo parsed contiene una estructura específica del programa, como los campos de una cuenta de token para el programa Token. Esta es la representación más conveniente cuando existe un parser, porque elimina la necesidad de un layout Borsh del lado del cliente.
La limitación crítica es la cobertura. El runtime solo parsea cuentas cuyo programa propietario conoce, como los programas System, Token, Token-2022 y Stake y otros built-ins similares. Para una cuenta cuyo propietario es un programa personalizado, el nodo no tiene parser, y el comportamiento documentado es que parsed es null. Los bytes sin procesar siguen siendo la única representación veraz en ese caso.
Este es el modo de fallo que pilla desprevenidos a los equipos. Un cliente que solo implementa la rama jsonParsed recibirá notificaciones de cuentas de programas personalizados, verá parsed: null y lanzará una excepción o se saltará silenciosamente la actualización. La suscripción está funcionando; el cliente está descartando datos. La solución es ramificar según la forma y recurrir a los bytes sin procesar, que es por lo que muchos equipos solicitan base64 para todo lo que pretenden indexar.
- value.data es un objeto con program y parsed.
- parsed es null cuando el runtime no tiene parser para el programa propietario.
- Los clientes deben ramificar según la forma en lugar de asumir que parsed está presente.
El sobre de AccountNotification es idéntico entre codificaciones
La forma del resultado de la notificación no cambia cuando cambias de codificación. Según la documentación de Solana, un accountNotification lleva un result con context: { slot } y value: { lamports, owner, data, executable, rentEpoch }. El único campo cuya forma varía es data. Con base64 y base64+zstd es una tupla; con jsonParsed es un objeto.
Esto importa para el diseño del cliente. Puedes escribir un único manejador para el sobre y aislar la lógica específica de la codificación en una sola función que normalice data a bytes o a un objeto parseado. Eso mantiene la ramificación contenida y hace obvio dónde podría romperse una suposición de forma.
También significa que no puedes inferir la codificación a partir del sobre. La etiqueta de codificación vive dentro de data para las formas de tupla, y la forma de objeto se identifica por sí misma. Un manejador robusto inspecciona data en lugar de confiar en un flag de configuración, porque una suscripción mal configurada es indistinguible de una brecha del parser.
- Sobre: context.slot más value con lamports, owner, data, executable, rentEpoch.
- Solo value.data cambia de forma entre codificaciones.
- Normaliza data en un solo lugar; mantén el resto del manejador agnóstico a la codificación.
Ejemplo ejecutable en Node.js: suscribirse con base64 y jsonParsed
El ejemplo siguiente abre dos suscripciones a la misma cuenta, una con encoding base64 y otra con jsonParsed, e imprime la forma de data que produce cada una. Usa el paquete ws y el fetch global para la lectura inicial de la cuenta. Reemplaza el endpoint y la pubkey de la cuenta por los tuyos. El objetivo es observar las formas en tu endpoint, no confiar en una captura de pantalla.
Fíjate en la función de normalización: detecta la forma de tupla, decodifica base64 e informa la longitud en bytes; detecta la forma de objeto e informa si parsed es null. Esa única función es el patrón que debes llevar a producción, porque hace visible la brecha del parser en lugar de silenciarla.
// npm install ws
import WebSocket from 'ws';
const WS_URL = process.env.SOLANA_WS_URL || 'wss://your-endpoint.example';
const ACCOUNT = process.env.ACCOUNT_PUBKEY || 'YourAccountPubkeyHere';
function normalizeData(data) {
if (Array.isArray(data)) {
const [payload, encoding] = data;
const bytes = Buffer.from(payload, 'base64');
return { kind: 'raw', encoding, byteLength: bytes.length };
}
if (data && typeof data === 'object') {
return {
kind: 'parsed',
program: data.program,
parsedIsNull: data.parsed === null,
parsed: data.parsed,
};
}
return { kind: 'unknown', data };
}
function subscribe(encoding) {
const ws = new WebSocket(WS_URL);
ws.on('open', () => {
ws.send(JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'accountSubscribe',
params: [ACCOUNT, { encoding, commitment: 'confirmed' }],
}));
});
ws.on('message', (raw) => {
const msg = JSON.parse(raw.toString());
if (msg.method === 'accountNotification') {
const { slot } = msg.params.result.context;
const { owner, data } = msg.params.result.value;
console.log(encoding, 'slot', slot, 'owner', owner, normalizeData(data));
} else {
console.log(encoding, 'control message', msg);
}
});
ws.on('error', (err) => console.error(encoding, 'ws error', err.message));
return ws;
}
const a = subscribe('base64');
const b = subscribe('jsonParsed');
// Close after 60s for a bounded experiment.
setTimeout(() => { a.close(); b.close(); }, 60000);Tabla de resultados: medir el comportamiento de la codificación en tu endpoint
La tabla siguiente es una plantilla, no un conjunto de valores medidos. Complétala con tu propio endpoint y cuenta, porque el tamaño en el cable, el trabajo del cliente y el comportamiento del parser dependen de la cuenta y de la versión del nodo. Ejecuta el ejemplo anterior, captura una notificación por codificación y registra los valores observados. No copies números de ningún artículo, incluido este.
Para el tamaño en el cable, registra la longitud del mensaje sin procesar antes de JSON.parse. Para el trabajo del cliente, anota si necesitaste un layout Borsh o una dependencia zstd. Para la sensibilidad del parser, compara la salida parsed entre dos versiones de nodo si puedes, o entre dos proveedores, y anota cualquier diferencia de campos. El objetivo es hacer concreto el tradeoff para tu carga de trabajo.
- Codificación | Tamaño en el cable (bytes) | Trabajo del cliente | Sensibilidad a la versión del parser | ¿parsed null observado?
- base64 | medir | Se requiere layout Borsh | ninguna (los bytes son estables) | n/a
- base64+zstd | medir | Layout Borsh más descompresor zstd | ninguna | n/a
- jsonParsed | medir | ninguna para programas soportados | sí, la salida del parser puede cambiar | sí para cuentas de programas personalizados
Costo de deserialización y deriva de la versión del parser
El tradeoff central es dónde ocurre la deserialización. base64 traslada el trabajo a tu cliente, que debe mantener un layout Borsh que coincida con el programa on-chain. Ese layout es una carga de mantenimiento, pero está versionado por ti, así que una actualización del nodo no puede cambiar silenciosamente el significado de tus datos. Por eso los indexadores en producción suelen solicitar base64 por estabilidad.
jsonParsed traslada el trabajo al nodo. Eso es conveniente y elimina la carga del layout, pero introduce una dependencia del parser del nodo. La salida del parser es específica del programa y puede cambiar cuando cambia el parser del nodo, lo que significa que la misma cuenta puede producir estructuras parseadas diferentes entre versiones de nodo o proveedores. Para inspección y depuración, eso está bien. Para un indexador de larga duración, es un riesgo de estabilidad.
Un patrón práctico es usar jsonParsed para inspección humana y scripts puntuales, y base64 para todo lo que alimenta una base de datos o un consumidor downstream. Si necesitas ambos, suscríbete dos veces y reconcilia, o suscríbete con base64 y parsea localmente con un layout que controles. La página de la API WebSocket de Solana es una referencia útil para la superficie del método cuando estés montando esto.
- base64: el cliente es dueño de la deserialización; estable entre actualizaciones del nodo.
- jsonParsed: el nodo es dueño de la deserialización; la salida puede derivar con cambios del parser.
- Patrón común: jsonParsed para inspección, base64 para indexación.
Solución de problemas: parsed null, discrepancias de forma e IDs de suscripción
Cuando parsed es null, la cuenta pertenece a un programa para el que el runtime no tiene parser. Este es un comportamiento documentado, no un error. La solución es recurrir a los bytes sin procesar, lo que significa que necesitas un layout Borsh para ese programa. Si no puedes mantener uno, considera si necesitas la cuenta en absoluto, o si un método diferente como getProgramAccounts con dataSlice y filtros se ajusta mejor al patrón de acceso.
Cuando veas una forma de tupla u objeto inesperada, revisa la etiqueta de codificación dentro de data. Una tupla con "base64" significa que te suscribiste con base64; un objeto significa jsonParsed. Si tu manejador asumió una y recibió la otra, la configuración de la suscripción y el manejador están desincronizados. Normaliza en un solo lugar y registra la forma en la primera notificación durante el desarrollo.
Para payloads comprimidos, confirma que la etiqueta es "base64+zstd" antes de descomprimir. Intentar decodificar en base64 un payload zstd sin descomprimirlo produce bytes basura que aún podrían deserializarse en algo sin sentido. Valida un campo conocido, como lamports, contra una lectura de getAccountInfo antes de confiar en la estructura decodificada.
Para la correlación del id de suscripción, guarda el id devuelto por la respuesta de accountSubscribe y compáralo con el campo subscription de cada notificación. Si abres varias suscripciones, incluidas a la misma cuenta con codificaciones diferentes, el id es la única forma confiable de saber cuál es cuál. El artículo sobre parseo de notificaciones de logsSubscribe cubre la misma disciplina de correlación para suscripciones de logs.
Si las notificaciones dejan de llegar, revisa el ciclo de vida de la conexión en lugar de la codificación. Las conexiones WebSocket pueden caerse, y una conexión caída significa que no hay notificaciones independientemente de la codificación. La lógica de reconexión y resuscripción se cubre en la guía de WebSocket de Solana.
- parsed null: no hay parser para el programa propietario; recurre a los bytes sin procesar.
- Discrepancia de forma: inspecciona la etiqueta de codificación dentro de data.
- Payload comprimido: descomprime antes de decodificar en base64; valida un campo conocido.
- Correlaciona las notificaciones por id de suscripción, no por orden de llegada.
Limitaciones: cobertura del parser, deriva de versiones y alcance de la cuenta
La cobertura de jsonParsed se limita a los programas que el runtime conoce. Los programas personalizados no se parsean, y el conjunto de programas parseados no es algo que tú controles. Trata jsonParsed como una conveniencia para programas soportados, no como una representación universal. Cualquier cliente que asuma un parseo universal descartará actualizaciones de cuentas de programas personalizados.
La salida del parser puede derivar entre versiones de nodo y proveedores. El comportamiento documentado es que las estructuras parseadas son específicas del programa; la consecuencia práctica es que no deberías tratar la salida parseada como un esquema estable para almacenamiento a largo plazo. Si necesitas estabilidad, base64 es el contrato más seguro porque los bytes son la serialización propia del programa.
accountSubscribe observa una única cuenta. No es una suscripción a nivel de programa y no te da la transacción que causó el cambio. Si necesitas la causa, combínalo con logsSubscribe o busca la transacción por firma. Si necesitas muchas cuentas, considera getProgramAccounts o un patrón de suscripción a programas, y ten en cuenta las características de costo y tasa de tu proveedor, que varían según el proveedor y el plan. Para opciones de endpoint, consulta Redes Solana y Precios de RPC.
- jsonParsed cubre solo los programas que el runtime conoce; los programas personalizados devuelven parsed null.
- La salida parseada puede derivar entre versiones de nodo y proveedores.
- accountSubscribe es por cuenta y no incluye la transacción causante.
Próximos pasos: elegir una codificación e integrarla en tu stack
Empieza ejecutando el ejemplo anterior contra tu endpoint y completando la tabla de resultados. Eso te da una base concreta para la decisión en lugar de una regla general. Si tu cuenta pertenece a un programa soportado y solo necesitas inspección, jsonParsed es conveniente. Si estás indexando, base64 es el valor por defecto estable, y base64+zstd vale la pena medirlo si el ancho de banda es una restricción.
Luego endurece el manejador: normaliza data en una sola función, ramifica según la forma, registra la etiqueta de codificación en la primera notificación y correlaciona por id de suscripción. Añade lógica de reconexión y resuscripción para que una conexión caída no detenga silenciosamente tus actualizaciones. Si estás construyendo sobre OnFinality, la referencia de la API WebSocket de Solana y la página del servicio de API describen la superficie del endpoint, y el hub de aprendizaje de OnFinality recopila guías relacionadas, incluida leer información de cuentas, rent y cuentas de token de Solana.
Por último, decide dónde vive el parseo en tu arquitectura. Si parseas localmente, versiona tus layouts Borsh junto con el programa y pruébalos contra cuentas conocidas. Si dependes de jsonParsed, fija tus expectativas a una versión de nodo y monitoriza la deriva. En cualquier caso, haz explícita la elección de codificación en la configuración y visible en los logs, para que la próxima persona que depure una discrepancia de forma pueda verla de inmediato.
- Mide primero: completa la tabla de resultados en tu endpoint.
- Endurece el manejador: normaliza, ramifica, registra, correlaciona.
- Decide dónde vive el parseo y versiónalo deliberadamente.