Anchor イベントは、プログラムの IDL に含まれる、型付けされた Borsh シリアライズ済みのレコードです。これにより、クライアントは人間が読めるログメッセージを解析することなく、プログラムのアクティビティを解釈できます。
イベントはインデックス作成や通知に便利ですが、プログラムの状態ではありません。プログラムが再度読み取る必要がある値は、アカウントに保存してください。イベントは、成功したトランザクションから派生した記録として扱ってください。
イベントの定義と発行
構造体に #[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 は 2 つの発行方式をサポートしています。
| 方式 | 保存場所 | トレードオフ |
|---|---|---|
emit! | プログラムデータログ | よりシンプルで低コストですが、ログが切り詰められる可能性があります |
emit_cpi! | CPI 由来の inner instruction data | メタデータの耐久性は高いものの、追加のアカウントとコンピュートが必要です |
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] は、署名付きの自己 CPI に必要な event authority と現在の program account を追加します。CPI イベントは Anchor のログイベントサブスクリプションでは取得できません。代わりに、トランザクションを取得し、一致する inner instruction をデコードしてください。
台帳を信頼できる情報源として使う
RPC プロバイダーはプログラムログを切り詰める場合があります。イベントを取りこぼすと、残高、権限、その他の正確性が重要なワークフローに影響する場合は、プログラムのアカウントまたはトランザクションの instruction data から結果を導出してください。イベントはインデックス作成を効率化するために使い、状態の唯一の記録として使わないでください。
TypeScript からのサブスクライブ
現在の Anchor TypeScript クライアントは @anchor-lang/core です。プログラムの IDL から作成した Program インスタンスは、一致するログイベントをデコードし、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);}
1 つのトランザクションが複数のプログラムを呼び出すことがあります。本番用のパーサーでは、Program <address> invoke と success のログ境界を追跡するか、Anchor のイベントパーサーを使用して、他のプログラムの Program data: エントリが自分のものとしてデコードされないようにしてください。失敗したトランザクションには、エラー前に完了した処理のログと inner instructions が残る場合があるため、いずれの形式のイベントもインデックスする前に、err が null でないトランザクションメタデータは必ず除外してください。Anchor のログサブスクリプションは、イベントコールバックを配信する前にこのチェックを行います。
emit_cpi! イベントの場合は、meta.innerInstructions から自己 CPI を見つけ、そのプログラム ID が自分のプログラムであることを確認したうえで、その instruction data を base58 デコードします。バイトを取り除く前に、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 とタグの両方を確認することで、通常の自己 CPI がイベントと誤認されるのを防げます。
イベントスキーマのバージョン管理
デフォルトでは、Anchor はイベント名から 8 バイトのイベントディスクリミネータを導出します。#[event(discriminator = ...)] による明示的なディスクリミネータの指定もサポートしています。ディスクリミネータはプログラム内で一意であり、イベントをデコードするすべてのクライアントに対して安定している必要があります。
発行する構造体のフィールドを変更すると、Borsh のレイアウトが変わります。互換性を損なうスキーマ変更では、PaymentRecorded をそのまま変更するのではなく、PaymentRecordedV2 のような新しいイベントを定義してください。既存のインデクサーがまだ旧イベントに依存している場合は、移行期間中も旧イベントの発行を続けてください。
進化していくデータには、固定幅のフィールドと、インデクサーがより詳細なアカウントデータを別途取得できる識別子を使うことをお勧めします。コンシューマーが新しいイベントをすぐにデコードできるよう、更新した 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?