Anchor-Events sind typisierte, mit Borsh serialisierte Datensätze, die in der IDL eines Programms enthalten sind. So können Clients die Programmaktivität interpretieren, ohne menschenlesbare Lognachrichten parsen zu müssen.
Events eignen sich für Indexierung und Benachrichtigungen, sind aber kein Programmzustand. Speichere jeden Wert, den das Programm erneut lesen muss, in Konten, und betrachte Events als einen Datensatz, der aus einer erfolgreichen Transaktion abgeleitet wird.
Ein Event definieren und ausgeben
Füge #[event] zu einer Struct hinzu und rufe emit! auf, nachdem die Zustandsänderung erfolgreich war.
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! schreibt die Event-Bytes mit dem Syscall sol_log_data von Solana. Die Transaktionslogs
zeigen das Ergebnis als Program data: <base64>. Der kodierte Wert beginnt mit
der Event-Discriminator, gefolgt von den mit Borsh serialisierten Feldern.
Halte Events schlank. Große Strings und Vektoren verbrauchen Compute, vergrößern die Transaktionsmetadaten und sind eher von Log-Kürzungen betroffen.
Zwischen Log- und CPI-Events wählen
Anchor unterstützt zwei Ausgabewege:
| Pfad | Speicherort | Kompromiss |
|---|---|---|
emit! | Programmdaten-Logs | Einfacher und günstiger, aber Logs können gekürzt werden |
emit_cpi! | Inner instruction data aus einem CPI | Dauerhaftere Metadaten, aber mit zusätzlichen Konten und höherem Compute-Verbrauch |
Um CPI-Events zu verwenden, aktiviere das Feature event-cpi von Anchor:
[dependencies]anchor-lang = { version = "1.1.2", features = ["event-cpi"] }
Annotiere dann die Konten-Struct der Anweisung mit #[event_cpi] und gib das Event
aus einem Handler aus, dessen Kontextvariable ctx heißt:
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] fügt die Event-Authority und die Konten des aktuellen Programms hinzu, die für
den signierten Self-CPI erforderlich sind. CPI-Events lassen sich nicht über das Log-Event-Abonnement
von Anchor konsumieren; rufe stattdessen die Transaktion ab und dekodiere die passende innere
Anweisung.
Das Ledger als Quelle verwenden
RPC-Provider können Programmlogs kürzen. Wenn ein fehlendes Event Guthaben, Berechtigungen oder einen anderen korrektheitskritischen Workflow beeinträchtigen würde, leite das Ergebnis aus den Konten des Programms oder den instruction data der Transaktion ab. Nutze Events, um die Indexierung effizient zu machen, nicht als einzige Aufzeichnung des Zustands.
Aus TypeScript abonnieren
Der aktuelle Anchor-TypeScript-Client ist @anchor-lang/core. Eine aus der Programm-IDL erzeugte Program-
Instanz kann passende Log-Events dekodieren und sie über das RPC-Log-Abonnement
zustellen:
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);
Der Callback erhält die dekodierten Event-Daten, den slot und die Transaktionssignatur. Abonnements erfassen nur Logs, die zugestellt werden, solange die Verbindung aktiv ist. Speichere verarbeitete Signaturen und hole nach einer Trennung verpasste Transaktionen nach, wenn der Consumer eine vollständige Historie benötigt.
Logs abgerufener Transaktionen dekodieren
Für Backfills und einmalige Prüfungen rufst du eine Transaktion ab und übergibst jede
Program data:-Payload an den Event-Coder:
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);}
Eine Transaktion kann mehrere Programme aufrufen. Verfolge in einem Produktions-Parser die
Log-Grenzen Program <address> invoke und success oder nutze den Event-Parser von Anchor,
damit der Program data:-Eintrag eines anderen Programms nicht als deiner dekodiert wird.
Fehlgeschlagene Transaktionen können Logs und innere Anweisungen von Arbeit behalten, die
vor dem Fehler abgeschlossen wurde. Weise daher Transaktionsmetadaten mit einem err ungleich null
immer ab, bevor du eine der beiden Event-Formen indexierst. Das Log-Abonnement von Anchor führt diese Prüfung
durch, bevor es einen Event-Callback auslöst.
Suche bei einem emit_cpi!-Event den Self-CPI in meta.innerInstructions,
bestätige, dass seine Programm-ID deinem Programm entspricht, und dekodiere dessen instruction data
per base58. Prüfe vor dem Entfernen von Bytes das
EVENT_IX_TAG_LE von 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);
Die Prüfung von Programm-ID und Tag verhindert, dass ein gewöhnlicher Self-CPI fälschlich für ein Event gehalten wird.
Event-Schemas versionieren
Standardmäßig leitet Anchor eine 8-Byte-Event-Discriminator aus dem Event-Namen ab.
Anchor unterstützt außerdem eine explizite Discriminator über
#[event(discriminator = ...)]. Discriminators müssen innerhalb des
Programms eindeutig und für jeden Client, der das Event dekodiert, stabil bleiben.
Das Ändern der Felder einer ausgegebenen Struct ändert deren Borsh-Layout. Definiere bei einer inkompatiblen
Schemaänderung ein neues Event wie PaymentRecordedV2, anstatt PaymentRecorded
stillschweigend zu ändern. Gib das alte Event während eines Migrationsfensters weiterhin aus,
wenn bestehende Indexer noch davon abhängen.
Bevorzuge für weiterentwickelbare Daten Felder fester Breite und Bezeichner, mit denen ein Indexer umfangreichere Kontodaten separat abrufen kann. Veröffentliche die aktualisierte IDL vor oder zusammen mit dem Programm-Upgrade, damit Consumer das neue Event sofort dekodieren können.
Events testen
Teste den dekodierten Vertrag, nicht den base64-String. Registriere den Listener, bevor du die Anweisung sendest, prüfe die dekodierten Felder und die Transaktionssignatur und entferne den Listener immer:
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);}
Teste außerdem, dass der Indexer Event-Bytes ignoriert, die in den Metadaten fehlgeschlagener Transaktionen erhalten bleiben,
und füge für jede unterstützte Event-Version eine Fixture hinzu. Wenn du
emit_cpi! verwendest, teste die Dekodierung aus innerInstructions, da Log-Listener
diese Events nicht direkt empfangen.
Weiterführende Links
Is this page helpful?