Події Anchor — це типізовані записи, серіалізовані за допомогою Borsh, які включаються до IDL програми. Вони дають клієнтам змогу інтерпретувати активність програми без розбору зрозумілих людині повідомлень журналу.
Події корисні для індексації та сповіщень, але вони не є станом програми. Зберігайте в акаунті будь-яке значення, яке програма має прочитати повторно, а події розглядайте як запис, отриманий із успішної транзакції.
Визначення та генерація події
Додайте #[event] до структури та викличте emit! після успішної зміни стану.
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! записує байти події за допомогою системного виклику Solana sol_log_data. Журнали
транзакції показують результат як Program data: <base64>. Закодоване значення починається з
дискримінатора події, за яким ідуть її поля, серіалізовані за допомогою Borsh.
Робіть події лаконічними. Великі рядки та вектори споживають обчислювальні ресурси, збільшують метадані транзакції та з більшою ймовірністю постраждають від усічення журналу.
Вибір між подіями в журналі та CPI-подіями
Anchor підтримує два способи генерації подій:
| Спосіб | Місце зберігання | Компроміс |
|---|---|---|
emit! | Журнали program data | Простіше й дешевше, але журнали можуть бути усічені |
emit_cpi! | Дані внутрішньої інструкції з CPI | Надійніші метадані, але потрібні додаткові акаунти та обчислювальні ресурси |
Щоб використовувати CPI-події, увімкніть функцію Anchor event-cpi:
[dependencies]anchor-lang = { version = "1.1.2", features = ["event-cpi"] }
Потім позначте структуру акаунтів інструкції атрибутом #[event_cpi] та генеруйте подію
в обробнику, змінна контексту якого має назву 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] додає акаунт event authority і акаунти поточної програми, необхідні для
підписаного self-CPI. CPI-події не можна отримувати через підписку на події журналу
Anchor; натомість отримайте транзакцію та декодуйте відповідну внутрішню
інструкцію.
Використовуйте реєстр як джерело істини
RPC-провайдери можуть усікати журнали програм. Якщо пропуск події вплине на баланси, дозволи чи інший критично важливий для коректності робочий процес, отримуйте результат з акаунтів програми або з даних інструкцій транзакції. Використовуйте події, щоб зробити індексацію ефективною, а не як єдиний запис стану.
Підписка з TypeScript
Поточний TypeScript-клієнт Anchor — @anchor-lang/core. Екземпляр Program,
створений на основі IDL програми, може декодувати відповідні події журналу та доставляти
їх через підписку на журнали 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);
Колбек отримує декодовані дані події, slot і підпис транзакції. Підписки охоплюють лише журнали, доставлені, поки з’єднання активне. Зберігайте оброблені підписи та донаповнюйте пропущені транзакції після розриву з’єднання, якщо споживачу потрібна повна історія.
Декодування журналів отриманої транзакції
Для донаповнення та разової перевірки отримайте транзакцію та передайте кожне
корисне навантаження Program data: кодеру подій:
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);}
Транзакція може викликати кілька програм. Для продакшн-парсера відстежуйте межі
журналу Program <address> invoke і success або використовуйте парсер подій Anchor, щоб запис Program data:
іншої програми не був декодований як ваш.
Невдалі транзакції можуть зберігати журнали та внутрішні інструкції від роботи, виконаної
до помилки, тому завжди відхиляйте метадані транзакції з ненульовим err
перед індексацією будь-якої форми події. Підписка на журнали Anchor виконує цю перевірку
перед викликом колбека події.
Для події emit_cpi! знайдіть self-CPI в meta.innerInstructions,
переконайтеся, що його ID програми збігається з вашою програмою, і декодуйте з base58 його instruction
data. Перед відсіканням будь-яких байтів перевірте Anchor
EVENT_IX_TAG_LE:
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);
Перевірка і ID програми, і тегу запобігає тому, щоб звичайний self-CPI було помилково сприйнято за подію.
Версіонування схем подій
За замовчуванням Anchor виводить 8-байтовий дискримінатор події з її назви.
Anchor також підтримує явний дискримінатор через
#[event(discriminator = ...)]. Дискримінатори мають залишатися унікальними в межах
програми та стабільними для кожного клієнта, який декодує подію.
Зміна полів згенерованої структури змінює її розмітку Borsh. Для несумісної
зміни схеми визначайте нову подію, наприклад PaymentRecordedV2, а не
тихо змінюйте PaymentRecorded. Продовжуйте генерувати стару подію протягом
періоду міграції, якщо наявні індексатори досі від неї залежать.
Для даних, що розвиваються, надавайте перевагу полям фіксованої ширини та ідентифікаторам, які дозволяють індексатору окремо отримувати розширені дані акаунта. Публікуйте оновлений IDL до або разом з оновленням програми, щоб споживачі могли одразу декодувати нову подію.
Тестування подій
Тестуйте декодований контракт, а не рядок base64. Зареєструйте слухача до надсилання інструкції, перевірте декодовані поля та підпис транзакції і завжди видаляйте слухача:
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);}
Також перевірте, що індексатор ігнорує байти подій, збережені в метаданих невдалих
транзакцій, і додайте фікстуру для кожної підтримуваної версії події. Якщо ви використовуєте
emit_cpi!, тестуйте декодування з innerInstructions, оскільки слухачі журналів не
отримують ці події безпосередньо.
Додаткові матеріали
Is this page helpful?