Dokumentacja SolanaTworzenie programów

Zdarzenia Anchor

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.

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! 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żkaMiejsce przechowywaniaKompromis
emit!Logi Program dataProstsze i tańsze, ale logi mogą zostać obcięte
emit_cpi!Inner instruction data z wywołania CPITrwalsze metadane, ale wymagają dodatkowych kont i mocy obliczeniowej

Aby używać zdarzeń CPI, włącz funkcję event-cpi w Anchor:

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

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] 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?

Spis treści

Edytuj stronę