أحداث Anchor هي سجلات مُنمَّطة ومُسلسَلة بصيغة Borsh ومُضمَّنة في IDL الخاص بالبرنامج. تتيح للعملاء تفسير نشاط البرنامج دون الحاجة إلى تحليل رسائل السجل المقروءة للبشر.
الأحداث مفيدة للفهرسة والإشعارات، لكنها ليست حالة للبرنامج. خزِّن في حساب أي قيمة يحتاج البرنامج إلى قراءتها مرة أخرى، وتعامل مع الأحداث بوصفها سجلاً مشتقاً من معاملة ناجحة.
تعريف حدث وإصداره
أضف #[event] إلى بنية (struct) واستدعِ 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! بايتات الحدث باستخدام استدعاء النظام sol_log_data في سولانا. تعرض
سجلات المعاملة النتيجة على هيئة Program data: <base64>. تبدأ القيمة المُرمَّزة بمعرّف
الحدث (discriminator) متبوعاً بحقوله المُسلسَلة بصيغة Borsh.
اجعل الأحداث مركّزة. فالسلاسل النصية والمتجهات الكبيرة تستهلك موارد الحوسبة، وتزيد من البيانات الوصفية للمعاملة، وتكون أكثر عرضة للتأثر باقتطاع السجلات.
الاختيار بين أحداث السجل وأحداث CPI
يدعم Anchor مسارين لإصدار الأحداث:
| المسار | موقع التخزين | المفاضلة |
|---|---|---|
emit! | سجلات بيانات البرنامج | أبسط وأرخص، لكن قد يتم اقتطاع السجلات |
emit_cpi! | instruction data الداخلية من استدعاء CPI | بيانات وصفية أكثر متانة، مع حسابات وموارد حوسبة إضافية |
لاستخدام أحداث CPI، فعِّل الميزة event-cpi في Anchor:
[dependencies]anchor-lang = { version = "1.1.2", features = ["event-cpi"] }
ثم أضف التعليق التوضيحي #[event_cpi] إلى بنية حسابات التعليمة، وأصدِر الحدث
من معالج (handler) يكون اسم متغير السياق فيه 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 وحساب program account الحالي المطلوبَين
لاستدعاء self-CPI الموقَّع. لا يمكن استهلاك أحداث CPI عبر اشتراك أحداث
السجل في Anchor؛ بدلاً من ذلك، اجلب المعاملة وفك ترميز التعليمة الداخلية
المطابقة.
اعتمد دفتر الأستاذ (ledger) مصدراً
قد يقتطع مزودو RPC سجلات البرنامج. إذا كان فقدان أحد الأحداث سيؤثر على الأرصدة أو الأذونات أو أي سير عمل آخر يتوقف عليه سلامة النتائج، فاشتق النتيجة من حسابات البرنامج أو من instruction data الخاصة بالمعاملة. استخدم الأحداث لجعل الفهرسة أكثر كفاءة، وليس بوصفها السجل الوحيد للحالة.
الاشتراك من TypeScript
عميل Anchor الحالي للغة TypeScript هو @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);
تتلقى دالة الاستدعاء (callback) بيانات الحدث بعد فك ترميزها، وال slot، وتوقيع المعاملة. تغطي الاشتراكات السجلات التي تُسلَّم أثناء اتصال نشط فقط. احفظ التوقيعات التي عولجت واستعد المعاملات الفائتة بعد انقطاع الاتصال إذا كان المستهلك يحتاج إلى السجل الكامل.
فك ترميز سجلات المعاملات المجلوبة
لاستعادة البيانات الفائتة والفحص لمرة واحدة، اجلب المعاملة ومرِّر كل حمولة
Program data: إلى مُرمِّز الأحداث (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);}
قد تستدعي المعاملة الواحدة عدة برامج. في المحلل الخاص بالإنتاج، تتبَّع حدود سجلات
Program <address> invoke وsuccess أو استخدم محلل أحداث Anchor
حتى لا يُفك ترميز إدخال Program data: الخاص ببرنامج آخر على أنه يخص برنامجك.
قد تحتفظ المعاملات الفاشلة بالسجلات والتعليمات الداخلية الناتجة عن أعمال اكتملت
قبل حدوث الخطأ، لذا ارفض دائماً البيانات الوصفية للمعاملة التي يكون فيها err غير فارغ
قبل فهرسة أي من شكلي الحدث. يجري اشتراك السجلات في Anchor هذا الفحص
قبل تسليم دالة الاستدعاء الخاصة بالحدث.
بالنسبة إلى حدث emit_cpi!، حدِّد موقع self-CPI في meta.innerInstructions،
وتأكد من أن معرّف برنامجه هو برنامجك، ثم فك ترميز instruction data الخاصة به بصيغة base58.
وقبل إزالة أي بايتات، تحقق من 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);
يمنع التحقق من معرّف البرنامج والوسم معاً اعتبار استدعاء self-CPI عادي حدثاً عن طريق الخطأ.
إدارة إصدارات مخططات الأحداث
افتراضياً، يشتق Anchor معرّف حدث (discriminator) من 8 بايتات انطلاقاً من اسم الحدث.
كما يدعم Anchor تحديد معرّف صريح عبر
#[event(discriminator = ...)]. يجب أن تبقى المعرّفات فريدة داخل
البرنامج وثابتة لكل عميل يفك ترميز الحدث.
يؤدي تغيير حقول البنية المُصدَرة إلى تغيير تخطيط Borsh الخاص بها. عند إجراء تغيير
جذري في المخطط، عرِّف حدثاً جديداً مثل PaymentRecordedV2 بدلاً من
تعديل PaymentRecorded بصمت. واصل إصدار الحدث القديم خلال فترة الترحيل
طالما كانت أدوات الفهرسة الحالية تعتمد عليه.
بالنسبة إلى البيانات القابلة للتطور، فضِّل الحقول ذات العرض الثابت والمعرّفات التي تتيح لأداة الفهرسة جلب بيانات الحساب الأغنى بشكل منفصل. انشر IDL المحدَّث قبل ترقية البرنامج أو معها حتى يتمكن المستهلكون من فك ترميز الحدث الجديد فوراً.
اختبار الأحداث
اختبر العقد بعد فك ترميزه، وليس سلسلة base64. سجِّل المستمع (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);}
اختبر أيضاً أن أداة الفهرسة تتجاهل بايتات الأحداث المحتفَظ بها في البيانات الوصفية
للمعاملات الفاشلة، وأضف حالة اختبار ثابتة (fixture) لكل إصدار حدث مدعوم. وإذا كنت تستخدم
emit_cpi!، فاختبر فك الترميز من innerInstructions لأن مستمعي السجلات لا
يتلقون هذه الأحداث مباشرة.
قراءات إضافية
Is this page helpful?