Dokumentasi SolanaMengembangkan Program

Event Anchor

Event Anchor adalah catatan bertipe yang diserialisasi dengan Borsh dan disertakan dalam IDL program. Event memungkinkan klien menafsirkan aktivitas program tanpa perlu mengurai pesan log yang dapat dibaca manusia.

Event berguna untuk pengindeksan dan notifikasi, tetapi bukan state program. Simpan setiap nilai yang harus dibaca kembali oleh program di dalam akun, dan perlakukan event sebagai catatan turunan dari transaksi yang berhasil.

Mendefinisikan dan memancarkan event

Tambahkan #[event] pada sebuah struct dan panggil emit! setelah perubahan state berhasil.

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! menulis byte event menggunakan syscall sol_log_data milik Solana. Log transaksi menampilkan hasilnya sebagai Program data: <base64>. Nilai yang dienkode diawali dengan discriminator event, diikuti field-nya yang diserialisasi dengan Borsh.

Buat event tetap fokus. String dan vektor yang besar menghabiskan compute, memperbesar metadata transaksi, dan lebih mungkin terkena pemotongan log.

Memilih antara event log dan event CPI

Anchor mendukung dua jalur emisi:

JalurLokasi penyimpananPertimbangan
emit!Log data programLebih sederhana dan murah, tetapi log dapat terpotong
emit_cpi!Data instruksi inner dari sebuah CPIMetadata lebih tahan lama, dengan akun dan compute tambahan

Untuk menggunakan event CPI, aktifkan fitur event-cpi milik Anchor:

Cargo.toml
[dependencies]
anchor-lang = { version = "1.1.2", features = ["event-cpi"] }

Kemudian anotasi struct akun instruksi dengan #[event_cpi] dan pancarkan dari handler yang variabel kontekstnya bernama 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] menambahkan akun event authority dan akun program saat ini yang diperlukan oleh CPI mandiri yang ditandatangani. Event CPI tidak dapat dikonsumsi melalui langganan event log Anchor; sebagai gantinya, ambil transaksinya dan dekode inner instruction yang sesuai.

Gunakan ledger sebagai sumber

Penyedia RPC dapat memotong log program. Jika kehilangan sebuah event akan memengaruhi saldo, izin, atau alur kerja lain yang kritis terhadap kebenaran, turunkan hasilnya dari akun program atau data instruksi transaksi. Gunakan event untuk membuat pengindeksan efisien, bukan sebagai satu-satunya catatan state.

Berlangganan dari TypeScript

Klien TypeScript Anchor saat ini adalah @anchor-lang/core. Instance Program yang dibuat dari IDL program dapat mendekode event log yang cocok dan mengirimkannya melalui langganan log 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 menerima data event yang sudah didekode, slot, dan signature transaksi. Langganan hanya mencakup log yang dikirim selama koneksi aktif. Simpan signature yang sudah diproses dan isi ulang transaksi yang terlewat setelah koneksi terputus jika konsumen membutuhkan riwayat yang lengkap.

Mendekode log transaksi yang diambil

Untuk backfill dan inspeksi satu kali, ambil transaksi dan teruskan setiap payload Program data: ke 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);
}

Satu transaksi dapat memanggil beberapa program. Untuk parser produksi, lacak batas log Program <address> invoke dan success, atau gunakan event parser Anchor agar entri Program data: dari program lain tidak didekode sebagai milik Anda. Transaksi yang gagal dapat menyimpan log dan inner instruction dari pekerjaan yang selesai sebelum error, jadi selalu tolak metadata transaksi dengan err non-null sebelum mengindeks kedua bentuk event. Langganan log Anchor melakukan pemeriksaan ini sebelum mengirimkan callback event.

Untuk event emit_cpi!, cari self-CPI di meta.innerInstructions, pastikan program ID-nya adalah program Anda, dan dekode base58 data instruksinya. Sebelum membuang byte apa pun, verifikasi EVENT_IX_TAG_LE milik 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);

Memeriksa program ID sekaligus tag mencegah self-CPI biasa agar tidak keliru dianggap sebagai event.

Membuat versi skema event

Secara default, Anchor menurunkan discriminator event 8 byte dari nama event. Anchor juga mendukung discriminator eksplisit melalui #[event(discriminator = ...)]. Discriminator harus tetap unik di dalam program dan stabil bagi setiap klien yang mendekode event tersebut.

Mengubah field pada struct yang dipancarkan akan mengubah layout Borsh-nya. Untuk perubahan skema yang bersifat breaking, definisikan event baru seperti PaymentRecordedV2 alih-alih diam-diam mengubah PaymentRecorded. Tetap pancarkan event lama selama masa migrasi jika indexer yang ada masih bergantung padanya.

Untuk data yang dapat berevolusi, pilih field berlebar tetap dan identifier yang memungkinkan indexer mengambil data akun yang lebih kaya secara terpisah. Publikasikan IDL yang diperbarui sebelum atau bersamaan dengan upgrade program agar konsumen dapat langsung mendekode event baru.

Menguji event

Uji kontrak yang sudah didekode, bukan string base64-nya. Daftarkan listener sebelum mengirim instruksi, periksa field yang sudah didekode dan signature transaksi, lalu selalu hapus listener-nya:

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);
}

Uji juga bahwa indexer mengabaikan byte event yang tersimpan di metadata transaksi yang gagal, dan tambahkan fixture untuk setiap versi event yang didukung. Jika Anda menggunakan emit_cpi!, uji proses dekode dari innerInstructions karena listener log tidak menerima event tersebut secara langsung.

Bacaan lanjutan

Is this page helpful?

Daftar Isi

Edit Halaman
© 2026 Yayasan Solana. Semua hak dilindungi.