Zdarzenia Anchor to typowane rekordy serializowane za pomocą Borsh, uwzględnione w IDL programu. Pozwalają klientom interpretować aktywność programu bez parsowania czytelnych dla człowieka komunikatów logów.
Zdarzenia są przydatne do indeksowania i powiadomień, ale nie stanowią stanu programu. Każdą wartość, którą program musi odczytać ponownie, przechowuj na koncie, a zdarzenia traktuj jako zapis wynikający z udanej transakcji.
Definiowanie i emitowanie zdarzenia
Dodaj #[event] do struktury i wywołaj emit! po pomyślnej zmianie stanu.
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! zapisuje bajty zdarzenia za pomocą wywołania systemowego Solany sol_log_data. Logi
transakcji pokazują wynik jako Program data: <base64>. Zakodowana wartość zaczyna się od
dyskryminatora zdarzenia, po którym następują jego pola serializowane w Borsh.
Twórz zwięzłe zdarzenia. Duże ciągi znaków i wektory zużywają jednostki obliczeniowe, zwiększają metadane transakcji i są bardziej narażone na skutki obcinania logów.
Wybór między zdarzeniami w logach a zdarzeniami CPI
Anchor obsługuje dwie ścieżki emisji:
| Ścieżka | Miejsce przechowywania | Kompromis |
|---|---|---|
emit! | Logi Program data | Prostsze i tańsze, ale logi mogą zostać obcięte |
emit_cpi! | Inner instruction data z wywołania CPI | Trwalsze metadane, ale wymagają dodatkowych kont i mocy obliczeniowej |
Aby używać zdarzeń CPI, włącz funkcję event-cpi w Anchor:
[dependencies]anchor-lang = { version = "1.1.2", features = ["event-cpi"] }
Następnie oznacz strukturę kont instrukcji atrybutem #[event_cpi] i emituj zdarzenie
z handlera, którego zmienna kontekstu nazywa się 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] dodaje konto event authority oraz program account bieżącego programu, wymagane przez
podpisane self-CPI. Zdarzeń CPI nie można odbierać przez subskrypcję zdarzeń z logów
Anchor; zamiast tego pobierz transakcję i zdekoduj pasującą instrukcję
wewnętrzną.
Traktuj ledger jako źródło danych
Dostawcy RPC mogą obcinać logi programów. Jeśli pominięcie zdarzenia miałoby wpływ na salda, uprawnienia lub inny proces krytyczny dla poprawności, wyprowadź wynik z kont programu lub z instruction data transakcji. Używaj zdarzeń, aby indeksowanie było wydajne, a nie jako jedynego zapisu stanu.
Subskrypcja w TypeScript
Aktualnym klientem Anchor dla TypeScript jest @anchor-lang/core. Instancja Program
utworzona na podstawie IDL programu potrafi dekodować pasujące zdarzenia z logów i dostarczać
je przez subskrypcję logów 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);
Callback otrzymuje zdekodowane dane zdarzenia, slot oraz sygnaturę transakcji. Subskrypcje obejmują tylko logi dostarczone, gdy połączenie jest aktywne. Jeśli odbiorca potrzebuje pełnej historii, zapisuj przetworzone sygnatury i po rozłączeniu uzupełnij pominięte transakcje.
Dekodowanie logów pobranej transakcji
Aby uzupełnić dane wsteczne lub jednorazowo sprawdzić transakcję, pobierz ją i przekaż każdy
ładunek Program data: do kodera zdarzeń:
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);}
Transakcja może wywoływać kilka programów. W parserze produkcyjnym śledź granice
logów Program <address> invoke i success albo użyj parsera zdarzeń Anchor, aby wpis
Program data: innego programu nie został zdekodowany jako Twój.
Nieudane transakcje mogą zachowywać logi i instrukcje wewnętrzne z pracy wykonanej
przed wystąpieniem błędu, dlatego przed indeksowaniem którejkolwiek postaci zdarzenia zawsze odrzucaj metadane transakcji
z niepustym err. Subskrypcja logów Anchor wykonuje tę kontrolę
przed wywołaniem callbacku zdarzenia.
W przypadku zdarzenia emit_cpi! znajdź self-CPI w meta.innerInstructions,
potwierdź, że jego identyfikatorem programu jest Twój program, i zdekoduj jego instruction
data z base58. Zanim usuniesz jakiekolwiek bajty, zweryfikuj
EVENT_IX_TAG_LE 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);
Sprawdzenie zarówno identyfikatora programu, jak i tagu zapobiega pomyleniu zwykłego self-CPI ze zdarzeniem.
Wersjonowanie schematów zdarzeń
Domyślnie Anchor wyprowadza 8-bajtowy dyskryminator zdarzenia z jego nazwy.
Anchor obsługuje też jawnie podany dyskryminator przez
#[event(discriminator = ...)]. Dyskryminatory muszą pozostać unikalne w obrębie
programu i niezmienne dla każdego klienta, który dekoduje zdarzenie.
Zmiana pól emitowanej struktury zmienia jej układ w Borsh. W przypadku zmiany schematu
łamiącej zgodność zdefiniuj nowe zdarzenie, na przykład PaymentRecordedV2, zamiast
cicho zmieniać PaymentRecorded. Nadal emituj stare zdarzenie w okresie
migracji, jeśli istniejące indeksery wciąż od niego zależą.
W przypadku danych, które będą ewoluować, preferuj pola o stałej szerokości i identyfikatory, dzięki którym indekser może osobno pobrać bogatsze dane konta. Opublikuj zaktualizowany IDL przed aktualizacją programu lub razem z nią, aby odbiorcy mogli od razu dekodować nowe zdarzenie.
Testowanie zdarzeń
Testuj zdekodowany kontrakt, a nie ciąg base64. Zarejestruj listener przed wysłaniem instrukcji, sprawdź zdekodowane pola i sygnaturę transakcji, a na końcu zawsze usuń 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);}
Przetestuj też, czy indekser ignoruje bajty zdarzeń zachowane w metadanych nieudanych transakcji,
i dodaj fixture dla każdej obsługiwanej wersji zdarzenia. Jeśli używasz
emit_cpi!, przetestuj dekodowanie z innerInstructions, ponieważ listenery logów nie
otrzymują tych zdarzeń bezpośrednio.
Dalsza lektura
Is this page helpful?