Una notificación logsSubscribe de Solana es una notificación JSON-RPC 2.0: incluye jsonrpc, method y un objeto params con un id de suscripción y un resultado LogsResponse, y no tiene id porque el nodo la envía de forma no solicitada. El LogsResponse contiene context.slot más value.signature, value.err y value.logs; context.slot es la clave de ordenación al reensamblar un flujo, y value.err es null en caso de éxito o un objeto de error decodificado en caso de fallo. Dado que value.logs es un array plano de cadenas, correlacionar una instrucción con sus logs requiere rastrear un contador de profundidad a través de las líneas Program invoke y Program success/failed en lugar de leer un índice fijo. Los niveles de commitment pueden entregar el mismo slot más de una vez, por lo que los consumidores que necesitan exactamente una vez deben deduplicar por firma y preferir el commitment más fuerte observado. Esta guía separa el comportamiento documentado del protocolo de las recomendaciones y de un método de medición reproducible que puedes ejecutar contra tu propio endpoint.
El sobre de notificación PubSub y por qué no es una solicitud/respuesta
La API PubSub de WebSocket de Solana sigue la especificación JSON-RPC 2.0, donde una notificación es un objeto de solicitud sin id y el servidor no responde a ella. La especificación JSON-RPC 2.0 define este sobre como jsonrpc, method y params, sin campo id. Esa única omisión es la raíz de la mayoría de los errores de análisis: un cliente no puede correlacionar una notificación con un id JSON-RPC porque no hay un id por notificación que coincida.
En su lugar, la correlación ocurre a través del id de suscripción. Cuando envías una solicitud logsSubscribe, el nodo responde con un resultado que es el id de suscripción. Cada notificación posterior para esa suscripción lleva ese mismo id dentro de params.subscription. Tu cliente debe mantener un mapa desde el id de suscripción a su propio manejador local, y enrutar cada frame entrante a través de ese mapa. Este es el mismo modelo de correlación utilizado para otros métodos de streaming y se cubre con más profundidad en Correlación de ID de notificación JSON-RPC y ordenamiento por lotes.
Una consecuencia práctica es que una sola conexión WebSocket puede alojar muchas suscripciones, y los frames de diferentes suscripciones se intercalan arbitrariamente. Si analizas asumiendo que el siguiente frame pertenece a la última solicitud que enviaste, enrutarás mal los datos tan pronto como abras una segunda suscripción. El sobre se autodescribe solo a través de method y params.subscription, así que trata esos dos campos como tus claves de enrutamiento.
- jsonrpc: siempre la cadena "2.0".
- method: el nombre del método de suscripción, por ejemplo "logsNotification".
- params.subscription: el id de suscripción numérico devuelto por la llamada de suscripción.
- params.result: el payload LogsResponse para este evento.
- Sin campo id: el nodo no espera ni envía una respuesta para una notificación.
La forma de LogsResponse: context, slot, signature, err y logs
La documentación de Solana para logsSubscribe define el resultado de la notificación como un LogsResponse con dos campos de nivel superior: context y value. context contiene un número de slot. value contiene signature, err y logs. Ese es todo el contrato, y cada campo importa para un análisis correcto.
context.slot es la clave de ordenación para reensamblar un flujo. La firma identifica la transacción, pero las firmas no son monótonas y no te dicen el orden en que el nodo observó los eventos. Si estás construyendo una vista de actividad ordenada temporalmente, ordena o agrupa por context.slot primero, luego por firma dentro de un slot. Tratar la firma como una clave de ordenación produce un flujo que parece barajado bajo carga.
value.logs es un array plano de cadenas. No es un árbol estructurado, ni un array de objetos, ni está agrupado por instrucción. El nodo emite las mismas líneas textuales que verías en los mensajes de log de una transacción, en el orden en que el runtime las produjo. Cualquier estructura que desees, como qué logs pertenecen a qué instrucción, debe ser reconstruida por tu parser. La guía complementaria sobre decodificación de getTransaction meta: logs e instrucciones internas cubre el mismo formato de log desde el lado del RPC, y la lógica de decodificación es reutilizable.
- context.slot: el slot en el que el nodo observó la transacción; úsalo para ordenar.
- value.signature: la firma de la transacción; úsala para deduplicar.
- value.err: null en caso de éxito, o un objeto de error decodificado en caso de fallo.
- value.logs: un array plano de cadenas en el orden de emisión del runtime.
El campo err: null en caso de éxito y un objeto de fallo decodificado en caso contrario
value.err es null cuando la transacción tuvo éxito. Cuando la transacción falló, value.err es un objeto que refleja el err de la meta de la transacción. Una forma común es {InstructionError: [index, reason]}, donde index es el índice de instrucción basado en cero y reason es una cadena u objeto anidado que describe el fallo. Dado que la forma coincide con la meta de getTransaction, el mismo decodificador que ya usas para las respuestas RPC se puede reutilizar aquí sin modificaciones.
Aquí es donde vive un error sutil y costoso. Un cliente que trata cualquier err no nulo como un fallo de red transitorio y reintenta la misma transacción reenviará un fallo determinista. Si el error es un InstructionError causado por la lógica del programa, el reintento fallará de forma idéntica y puede consumir comisiones de nuevo. La interpretación correcta es que la transacción fue incluida y falló; el campo err es un resultado, no un error de transporte.
Distingue los problemas a nivel de transporte de los fallos en cadena. Una conexión WebSocket caída, un timeout o un error de análisis JSON es un problema de transporte. Un value.err no nulo dentro de una notificación bien formada es un resultado en cadena. Tu política de reintentos debe aplicarse solo al primero, y tu alerta debe tratar el segundo como un evento de negocio.
- null: la transacción tuvo éxito.
- {InstructionError: [index, reason]}: una instrucción específica falló.
- Pueden aparecer otras formas de objeto según la clase de fallo; decodifica de forma defensiva.
- Nunca reintentes una transacción solo porque value.err no sea nulo.
Reensamblar un array plano de logs con un contador de profundidad
Dado que value.logs es un array plano, la única forma fiable de asociar logs con una instrucción es rastrear la profundidad de anidamiento. El runtime emite líneas como "Program <ID> invoke [1]", "Program log: ...", "Program <ID> success" y "Program <ID> failed". El número entre corchetes en la línea invoke es la profundidad. Cada invoke incrementa la profundidad; cada success o failed la decrementa. Los logs emitidos mientras la profundidad es N pertenecen a la invocación abierta en profundidad N.
Un contador de profundidad te permite construir una vista estructurada: una pila de frames, cada uno con un id de programa, una profundidad y una lista de líneas de log. Cuando ves una línea invoke, apilas un frame. Cuando ves una línea Program log, la añades al frame superior. Cuando ves success o failed, desapilas el frame y lo adjuntas a su padre. Este es el mismo algoritmo utilizado para la decodificación de instrucciones internas y se describe en decodificación de getTransaction meta: logs e instrucciones internas.
No asumas un índice fijo. El número de líneas de log por instrucción varía según el presupuesto de cómputo, el comportamiento del programa y si el programa emite logs o no. Un parser que lee logs[3] como "el log de la primera instrucción" se romperá en la primera transacción que emita un número diferente de líneas. El seguimiento de profundidad es el único enfoque estable.
- Las líneas invoke abren un frame y llevan la profundidad entre corchetes.
- Las líneas Program log se añaden al frame superior actual.
- Las líneas success y failed cierran el frame actual.
- Las líneas de unidades de cómputo son informativas y no cambian la profundidad.
Niveles de commitment, firmas duplicadas y procesamiento exactamente una vez
logsSubscribe acepta un parámetro commitment, y la documentación de Solana señala que confirmed y finalized pueden entregar la notificación del mismo slot en momentos diferentes. Una notificación finalized puede reemplazar a una confirmed. Si te suscribes en confirmed y luego en finalized, o si te vuelves a suscribir tras una reconexión, puedes ver la misma firma más de una vez.
Un consumidor que necesita procesamiento exactamente una vez debe deduplicar por firma y preferir el commitment más fuerte observado. Mantén un mapa de firma al nivel de commitment más alto observado, y solo emite un evento aguas abajo cuando el commitment mejore o cuando la firma sea nueva. Esto es una recomendación, no una garantía del protocolo: el nodo no promete una única entrega por firma.
La advertencia sobre el commitment interactúa con el ordenamiento. Una notificación confirmed para el slot N puede llegar después de una notificación finalized para el slot N-1. Si tu consumidor aguas abajo asume slots monótonos, verá regresiones aparentes. Agrupa primero por nivel de commitment, luego ordena por context.slot dentro de cada grupo, y solo promueve una firma cuando su commitment se fortalece.
- confirmed y finalized pueden entregar el mismo slot.
- Deduplica por firma; prefiere el commitment más fuerte.
- No asumas context.slot monótono entre niveles de commitment.
- La resuscribirse tras una reconexión puede reproducir eventos recientes.
Ejemplo ejecutable en Node.js: suscribirse, decodificar y rastrear profundidad
El siguiente ejemplo abre un WebSocket, se suscribe con un filtro mentions, decodifica cada notificación, imprime firma, slot y err, y reensambla la profundidad de los logs. Usa el paquete ws y asume un endpoint WebSocket JSON-RPC de Solana. Reemplaza el endpoint con la URL de tu propio proveedor. El mismo patrón se aplica tanto si te conectas a un endpoint público como a uno gestionado como la red Solana de OnFinality.
El parser es intencionalmente mínimo. No deduplica por firma ni maneja la promoción de commitment; esos quedan como ejercicios descritos en la sección anterior. El objetivo es mostrar el contrato del sobre en código funcional para que puedas adaptarlo a tu propio pipeline.
const WebSocket = require('ws');
const ENDPOINT = process.env.SOLANA_WS_ENDPOINT;
const ws = new WebSocket(ENDPOINT);
let subscriptionId = null;
function parseLogs(logs) {
const stack = [];
const frames = [];
for (const line of logs) {
const invoke = line.match(/^Program (\S+) invoke \[(\d+)\]$/);
const done = line.match(/^Program (\S+) (success|failed)$/);
const log = line.match(/^Program log: (.*)$/);
if (invoke) {
stack.push({ programId: invoke[1], depth: Number(invoke[2]), logs: [] });
} else if (log && stack.length) {
stack[stack.length - 1].logs.push(log[1]);
} else if (done && stack.length) {
const frame = stack.pop();
frame.status = done[2];
frames.push(frame);
}
}
return frames;
}
ws.on('open', () => {
ws.send(JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'logsSubscribe',
params: [{ mentions: ['11111111111111111111111111111111'] }, { commitment: 'confirmed' }]
}));
});
ws.on('message', (raw) => {
const msg = JSON.parse(raw.toString());
if (msg.id === 1 && msg.result) {
subscriptionId = msg.result;
console.log('subscribed with id', subscriptionId);
return;
}
if (msg.method !== 'logsNotification') return;
if (msg.params.subscription !== subscriptionId) return;
const { context, value } = msg.params.result;
console.log('slot', context.slot, 'sig', value.signature, 'err', value.err);
const frames = parseLogs(value.logs);
for (const f of frames) {
console.log(' program', f.programId, 'depth', f.depth, 'status', f.status);
}
});
ws.on('close', () => console.log('closed'));
ws.on('error', (e) => console.error('error', e.message));Tabla de resultados: medir el comportamiento de las notificaciones contra tu propio endpoint
El comportamiento del proveedor varía. El contrato del protocolo está documentado, pero el momento de entrega, las tasas de duplicados y la reproducción tras reconexión son específicos del proveedor y deben medirse, no asumirse. La tabla a continuación es un método que ejecutas contra tu propio endpoint. Rellénala con los valores observados; no trates ninguna fila como una constante universal.
Ejecuta la suscripción durante una ventana fija, por ejemplo una hora, y registra los conteos. Repite en confirmed y finalized para comparar. Si operas múltiples endpoints, ejecuta el mismo script contra cada uno y compara. Esta es la única forma fiable de caracterizar tu propio comportamiento de entrega.
- URL del endpoint: el endpoint WebSocket que probaste.
- Commitment: confirmed o finalized.
- Ventana: marcas de tiempo de inicio y fin de la ejecución.
- Total de notificaciones recibidas: conteo de frames logsNotification.
- Firmas únicas: conteo de valores distintos de value.signature.
- Firmas duplicadas: total menos únicas.
- Conteo de err no nulo: notificaciones donde value.err no es nulo.
- Brecha máxima de slot: mayor diferencia entre valores consecutivos de context.slot.
- Reproducciones tras reconexión: firmas vistas de nuevo tras una reconexión forzada.
Solución de problemas: notificaciones vacías, duplicados y colisiones de id
Las notificaciones vacías normalmente significan que el filtro es demasiado estrecho o el commitment es demasiado estricto. Un filtro mentions solo coincide con transacciones que mencionan la pubkey dada. Si filtras por un id de programa, solo verás transacciones que mencionan ese programa, no todas las transacciones que el programa procesa. Amplía el filtro o suscríbete con all y filtra en el lado del cliente. También confirma que el id de suscripción se capturó antes de empezar a enrutar frames.
Las firmas duplicadas entre niveles de commitment son esperadas, no un error. Si te suscribes en confirmed y finalized, o si te vuelves a suscribir tras una reconexión, la misma firma puede aparecer más de una vez. Deduplica por firma y prefiere el commitment más fuerte. Si ves duplicados dentro de un solo nivel de commitment, verifica si tu cliente abrió dos suscripciones y está enrutando ambas al mismo manejador.
Un err no nulo que no es un fallo de red es un resultado en cadena. No reintentes la transacción. Decodifica el objeto de error, registra el índice de instrucción y la razón, y enrútalo a tu lógica de negocio. Si estás viendo una alta tasa de valores err no nulos, inspecciona la lógica del programa en lugar del transporte.
Las colisiones de id de suscripción tras una reconexión ocurren cuando un cliente reutiliza un id obsoleto o no limpia su mapa. Al reconectar, descarta todos los id de suscripción anteriores, vuelve a suscribirte y reconstruye el mapa a partir de las nuevas respuestas. Los detalles del ciclo de vida de la conexión se cubren en Métodos WebSocket de Solana RPC y ciclo de vida de la conexión.
- Flujo vacío: amplía el filtro o baja el commitment.
- Duplicados: deduplica por firma, prefiere el commitment más fuerte.
- err no nulo: fallo en cadena, no un error de transporte.
- Colisiones de id: limpia el mapa de suscripciones al reconectar.
Limitaciones y compensaciones del contrato logsSubscribe
logsSubscribe te da logs, no instrucciones estructuradas. Obtienes un array plano de cadenas y debes reconstruir la estructura tú mismo. Esto es flexible pero pone la carga de análisis en tu cliente, y cualquier cambio en el formato de los logs puede romper tu parser. El formato es estable en la práctica pero no es un esquema versionado.
El sobre de notificación no tiene id, por lo que no puedes usar la correlación de id JSON-RPC. Debes mantener tu propio mapa de suscripciones. Esto es simple pero fácil de equivocarse en escenarios de reconexión o múltiples suscripciones. La guía Correlación de ID de notificación JSON-RPC y ordenamiento por lotes cubre el patrón general.
Los niveles de commitment no garantizan la entrega exactamente una vez. Debes deduplicar y promover. Esto añade estado a tu consumidor y significa que no puedes tratar el flujo como una cola simple. Si necesitas garantías más fuertes, es posible que debas reconciliar contra getTransaction o un indexador separado.
Finalmente, la entrega por WebSocket es de mejor esfuerzo. El nodo puede descartar frames bajo carga, y tu cliente puede perder eventos durante una reconexión. Para pipelines críticos, trata logsSubscribe como una señal de baja latencia y reconcilia contra una fuente duradera. La página API WebSocket de Solana (Asistente RPC) y la guía Mecánica de filtros de suscripción WebSocket de Solana cubren compensaciones relacionadas.
- Los logs planos requieren reconstrucción de estructura en el cliente.
- Sin id significa que debes mantener tu propio mapa de suscripciones.
- Los niveles de commitment requieren lógica de deduplicación y promoción.
- La entrega por WebSocket es de mejor esfuerzo; reconcilia para pipelines críticos.
Próximos pasos: del análisis a los pipelines de producción
Una vez que puedes decodificar una notificación, el siguiente paso es hacer que tu consumidor sea resiliente. Añade deduplicación por firma, promoción de commitment y un búfer acotado para slots fuera de orden. Persiste el slot procesado más alto para poder reanudar tras un reinicio sin reprocesar todo.
Si operas a escala, considera un endpoint gestionado para reducir la sobrecarga operativa. La red Solana de OnFinality proporciona acceso WebSocket, y la página de precios de RPC describe los planes. Para equipos que quieren una interfaz de más alto nivel, el servicio de API y el centro de aprendizaje de OnFinality tienen guías relacionadas.
Finalmente, prueba tu parser contra tráfico real. Usa la tabla de resultados anterior para caracterizar tu endpoint, y revisa la guía Métodos WebSocket de Solana RPC y ciclo de vida de la conexión para patrones de reconexión y keepalive. La combinación de un análisis correcto del sobre y un manejo resiliente de la conexión es lo que hace que un pipeline de logsSubscribe esté listo para producción.
- Añade deduplicación, promoción de commitment y un búfer acotado.
- Persiste el slot procesado más alto para seguridad ante reinicios.
- Considera un endpoint gestionado para escalar.
- Prueba contra tráfico real y completa la tabla de resultados.