Documentación de SolanaDesarrollo de programas

Eventos de Anchor

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.

lib.rs
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íaUbicación de almacenamientoCompromiso
emit!Logs de datos del programaMás simple y económico, pero los logs pueden truncarse
emit_cpi!Instruction data de instrucciones internas de una CPIMetadatos más duraderos, con cuentas y cómputo adicionales

Para usar eventos CPI, habilita la característica event-cpi de Anchor:

Cargo.toml
[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:

lib.rs
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?

Tabla de Contenidos

Editar Página