Los eventos de Anchor son registros tipados y serializados con Borsh que se incluyen en el IDL de un programa. Permiten a los clientes interpretar la actividad del programa sin tener que analizar mensajes de log legibles para humanos.
Los eventos son útiles para la indexación y las notificaciones, pero no forman parte del estado del programa. Guarda en una cuenta cualquier valor que el programa deba volver a leer y trata los eventos como un registro derivado de una transacción exitosa.
Define y emite un evento
Añade #[event] a una struct y llama a emit! después de que el cambio de estado se haya realizado correctamente.
use anchor_lang::prelude::*;#[program]pub mod payments {use super::*;pub fn record_payment(ctx: Context<RecordPayment>, amount: u64) -> Result<()> {emit!(PaymentRecorded {authority: ctx.accounts.authority.key(),amount,});Ok(())}}#[derive(Accounts)]pub struct RecordPayment<'info> {pub authority: Signer<'info>,}#[event]pub struct PaymentRecorded {pub authority: Pubkey,pub amount: u64,}
emit! escribe los bytes del evento con la syscall sol_log_data de Solana. Los logs
de la transacción muestran el resultado como Program data: <base64>. El valor codificado comienza con
el discriminador del evento, seguido de sus campos serializados con Borsh.
Mantén los eventos concisos. Las cadenas y los vectores grandes consumen cómputo, aumentan los metadatos de la transacción y son más propensos a verse afectados por el truncamiento de logs.
Elige entre eventos de log y eventos CPI
Anchor admite dos vías de emisión:
| Vía | Ubicación de almacenamiento | Compromiso |
|---|---|---|
emit! | Logs de datos del programa | Más simple y económico, pero los logs pueden truncarse |
emit_cpi! | Instruction data de instrucciones internas de una CPI | Metadatos más duraderos, con cuentas y cómputo adicionales |
Para usar eventos CPI, habilita la característica event-cpi de Anchor:
[dependencies]anchor-lang = { version = "1.1.2", features = ["event-cpi"] }
A continuación, anota la struct de cuentas de la instrucción con #[event_cpi] y emite
desde un handler cuya variable de contexto se llame ctx:
pub fn record_payment(ctx: Context<RecordPayment>, amount: u64) -> Result<()> {emit_cpi!(PaymentRecorded {authority: ctx.accounts.authority.key(),amount,});Ok(())}#[event_cpi]#[derive(Accounts)]pub struct RecordPayment<'info> {pub authority: Signer<'info>,}
#[event_cpi] añade la autoridad de eventos y las cuentas del programa actual que requiere
la auto-CPI firmada. Los eventos CPI no se pueden consumir mediante la suscripción a eventos de log
de Anchor; en su lugar, obtén la transacción y decodifica la instrucción interna
correspondiente.
Usa el ledger como fuente
Los proveedores de RPC pueden truncar los logs del programa. Si perder un evento afectaría a los saldos, los permisos u otro flujo de trabajo crítico para la corrección, obtén el resultado a partir de las cuentas del programa o del instruction data de la transacción. Usa los eventos para que la indexación sea eficiente, no como único registro del estado.
Suscríbete desde TypeScript
El cliente actual de Anchor para TypeScript es @anchor-lang/core. Una instancia de Program
creada a partir del IDL del programa puede decodificar los eventos de log coincidentes y entregarlos
mediante la suscripción a logs de RPC:
const listenerId = program.addEventListener("PaymentRecorded",(event, slot, signature) => {console.log({ event, slot, signature });},"confirmed");// Remove the websocket subscription when it is no longer needed.await program.removeEventListener(listenerId);
El callback recibe los datos del evento decodificados, el slot y la firma de la transacción. Las suscripciones solo cubren los logs entregados mientras la conexión está activa. Guarda las firmas procesadas y rellena las transacciones perdidas después de una desconexión si el consumidor necesita el historial completo.
Decodifica los logs de transacciones obtenidas
Para rellenar datos históricos y para inspecciones puntuales, obtén una transacción y pasa cada
payload de Program data: al codificador de eventos:
const transaction = await connection.getTransaction(signature, {commitment: "confirmed",maxSupportedTransactionVersion: 0});if (!transaction?.meta || transaction.meta.err) {throw new Error("Transaction was not found or did not succeed");}for (const log of transaction.meta.logMessages ?? []) {const prefix = "Program data: ";if (!log.startsWith(prefix)) continue;const decoded = program.coder.events.decode(log.slice(prefix.length));if (decoded) console.log(decoded.name, decoded.data);}
Una transacción puede invocar varios programas. En un parser de producción, haz un seguimiento de los
límites de log Program <address> invoke y success, o usa el parser de eventos de Anchor, para que la entrada Program data: de otro programa no se decodifique como si fuera tuya.
Las transacciones fallidas pueden conservar logs e instrucciones internas de trabajo completado
antes del error, así que rechaza siempre los metadatos de transacción cuyo err no sea nulo
antes de indexar cualquiera de los dos tipos de evento. La suscripción a logs de Anchor realiza esta comprobación
antes de entregar un callback de evento.
Para un evento de emit_cpi!, localiza la auto-CPI en meta.innerInstructions,
confirma que su ID de programa sea el de tu programa y decodifica en base58 su instruction
data. Antes de eliminar ningún byte, verifica el
EVENT_IX_TAG_LE de Anchor:
import bs58 from "bs58";const EVENT_IX_TAG_LE = Buffer.from("e445a52e51cb9a1d", "hex");const instructionData = Buffer.from(bs58.decode(innerInstruction.data));if (!instructionData.subarray(0, 8).equals(EVENT_IX_TAG_LE)) {// This is an ordinary self-CPI, not an emit_cpi! event.continue;}const eventData = instructionData.subarray(8).toString("base64");const decoded = program.coder.events.decode(eventData);
Comprobar tanto el ID del programa como la etiqueta evita que una auto-CPI ordinaria se confunda con un evento.
Versiona los esquemas de eventos
De forma predeterminada, Anchor deriva un discriminador de evento de 8 bytes a partir del nombre del evento.
Anchor también admite un discriminador explícito mediante
#[event(discriminator = ...)]. Los discriminadores deben seguir siendo únicos dentro del
programa y estables para todos los clientes que decodifican el evento.
Cambiar los campos de una struct emitida modifica su disposición en Borsh. Para un cambio
de esquema incompatible, define un nuevo evento, como PaymentRecordedV2, en lugar de
modificar PaymentRecorded sin avisar. Sigue emitiendo el evento anterior durante una
ventana de migración mientras los indexadores existentes aún dependan de él.
Para datos que vayan a evolucionar, prefiere campos de ancho fijo e identificadores que permitan a un indexador obtener por separado datos de cuenta más completos. Publica el IDL actualizado antes de la actualización del programa o junto con ella, para que los consumidores puedan decodificar el nuevo evento de inmediato.
Prueba los eventos
Prueba el contrato decodificado, no la cadena en base64. Registra el listener antes de enviar la instrucción, verifica los campos decodificados y la firma de la transacción, y elimina siempre el listener:
let listenerId = 0;const received = new Promise<{event: { authority: PublicKey; amount: BN };signature: string;}>((resolve) => {listenerId = program.addEventListener("PaymentRecorded",(event, _slot, signature) => resolve({ event, signature }),"confirmed");});try {const signature = await program.methods.recordPayment(new BN(500)).rpc();const result = await received;assert.equal(result.signature, signature);assert.equal(result.event.amount.toString(), "500");} finally {await program.removeEventListener(listenerId);}
Prueba también que el indexador ignore los bytes de eventos retenidos en los metadatos de transacciones
fallidas, y añade un fixture para cada versión de evento compatible. Si usas
emit_cpi!, prueba la decodificación desde innerInstructions, porque los listeners de logs no
reciben esos eventos directamente.
Lecturas adicionales
Is this page helpful?