6. Events

Learn how to emit and listen to program events for logging and real-time updates.


What are Events?

Events allow your program to:

  • Log important actions on-chain
  • Notify clients in real-time
  • Track history without storing everything in accounts
  • Trigger off-chain actions (indexers, bots, UI updates)

Events are cheap (only cost transaction fees) and efficient (no account storage needed).


Define Events

Source: events.rs

use anchor_lang::prelude::*;

#[event]
pub struct TokensMintedEvent {
    pub mint: Pubkey,
    pub recipient: Pubkey,
    pub amount: u64,
    pub timestamp: i64,
}

#[event]
pub struct UserAccountCreatedEvent {
    pub user: Pubkey,
    pub authority: Pubkey,
    pub timestamp: i64,
}

#[event]
pub struct TreasuryDepositEvent {
    pub treasury: Pubkey,
    pub depositor: Pubkey,
    pub amount: u64,
    pub total_deposited: u64,
    pub timestamp: i64,
}

#[event]
pub struct RoleAssignedEvent {
    pub authority: Pubkey,
    pub role_type: RoleType,
    pub assigned_by: Pubkey,
    pub timestamp: i64,
}

Event Anatomy

#[event]
pub struct MyEvent {
    pub field1: Type1,  // Any serializable type
    pub field2: Type2,
    // ... more fields
}

Supported types:

  • Primitives: u8, u64, i64, bool, etc.
  • Pubkey
  • String
  • Custom enums (with proper derives)
  • Nested structs (with proper derives)

Emit Events

Use the emit! macro to emit events:

Source: instructions/token.rs

use crate::events::*;

pub fn mint_tokens_handler(ctx: Context<MintTokens>, amount: u64) -> Result<()> {
    // Mint tokens logic...
    let cpi_accounts = MintTo {
        mint: ctx.accounts.mint.to_account_info(),
        to: ctx.accounts.token_account.to_account_info(),
        authority: ctx.accounts.mint_authority.to_account_info(),
    };

    let signer_seeds: &[&[&[u8]]] = &[&[
        SEED_MINT_AUTHORITY,
        &[ctx.bumps.mint_authority]
    ]];

    let cpi_ctx = CpiContext::new(
        ctx.accounts.token_program.to_account_info(),
        cpi_accounts
    ).with_signer(signer_seeds);

    token_interface::mint_to(cpi_ctx, amount)?;

    // Emit event after successful operation
    emit!(TokensMintedEvent {
        mint: ctx.accounts.mint.key(),
        recipient: ctx.accounts.token_account.key(),
        amount,
        timestamp: Clock::get()?.unix_timestamp,
    });

    msg!("Minted {} tokens", amount);
    Ok(())
}

When to Emit Events

✅ After successful operations - State changes committed
✅ Before returning Ok(()) - Ensure operation completed
✅ Include relevant data - All info needed by listeners
✅ Add timestamp - Track when events occurred

❌ Don’t emit before validation - Event may be emitted then tx fails
❌ Don’t emit sensitive data - Events are public
❌ Don’t emit too much - Keep events focused


Event Examples

User Account Creation

pub fn create_user_account_handler(ctx: Context<CreateUserAccount>) -> Result<()> {
    let user = &mut ctx.accounts.user_account;
    let clock = Clock::get()?;
    
    user.authority = ctx.accounts.authority.key();
    user.points = 0;
    user.created_at = clock.unix_timestamp;
    user.updated_at = clock.unix_timestamp;
    user.bump = ctx.bumps.user_account;

    emit!(UserAccountCreatedEvent {
        user: user.key(),
        authority: user.authority,
        timestamp: clock.unix_timestamp,
    });

    Ok(())
}

Treasury Deposit

pub fn deposit_to_treasury_handler(
    ctx: Context<DepositToTreasury>,
    amount: u64
) -> Result<()> {
    let treasury = &mut ctx.accounts.treasury;
    
    // Transfer SOL
    transfer(
        CpiContext::new(
            ctx.accounts.system_program.to_account_info(),
            Transfer {
                from: ctx.accounts.depositor.to_account_info(),
                to: treasury.to_account_info(),
            },
        ),
        amount,
    )?;

    // Update state
    treasury.total_deposited = treasury
        .total_deposited
        .checked_add(amount)
        .ok_or(ErrorCode::ArithmeticOverflow)?;

    // Emit event
    emit!(TreasuryDepositEvent {
        treasury: treasury.key(),
        depositor: ctx.accounts.depositor.key(),
        amount,
        total_deposited: treasury.total_deposited,
        timestamp: Clock::get()?.unix_timestamp,
    });

    Ok(())
}

Role Assignment

pub fn assign_role_handler(
    ctx: Context<AssignRole>,
    role_type: RoleType
) -> Result<()> {
    let role = &mut ctx.accounts.role;
    let clock = Clock::get()?;

    role.authority = ctx.accounts.target_authority.key();
    role.role_type = role_type;
    role.permissions = role_type.default_permissions();
    role.assigned_by = ctx.accounts.admin.key();
    role.assigned_at = clock.unix_timestamp;
    role.updated_at = clock.unix_timestamp;
    role.bump = ctx.bumps.role;

    emit!(RoleAssignedEvent {
        authority: role.authority,
        role_type,
        assigned_by: role.assigned_by,
        timestamp: clock.unix_timestamp,
    });

    Ok(())
}

Listen to Events (Client-Side)

TypeScript - Add Event Listener

import { Program } from "@coral-xyz/anchor";
import { StarterProgram } from "../target/types/starter_program";

const program = anchor.workspace.StarterProgram as Program<StarterProgram>;

// Add event listener
const listener = program.addEventListener(
  "TokensMintedEvent",
  (event, slot) => {
    console.log("🎉 Tokens minted!");
    console.log("  Mint:", event.mint.toString());
    console.log("  Recipient:", event.recipient.toString());
    console.log("  Amount:", event.amount.toString());
    console.log("  Timestamp:", new Date(event.timestamp * 1000).toISOString());
    console.log("  Slot:", slot);
  }
);

// Keep listener active...

// Remove listener when done
await program.removeEventListener(listener);

Multiple Event Listeners

// Listen to multiple event types
const mintListener = program.addEventListener("TokensMintedEvent", (event) => {
  console.log("Tokens minted:", event.amount.toString());
});

const userListener = program.addEventListener("UserAccountCreatedEvent", (event) => {
  console.log("User created:", event.user.toString());
});

const depositListener = program.addEventListener("TreasuryDepositEvent", (event) => {
  console.log("Treasury deposit:", event.amount.toString());
});

// Clean up all listeners
await program.removeEventListener(mintListener);
await program.removeEventListener(userListener);
await program.removeEventListener(depositListener);

Wait for Specific Event

async function waitForMintEvent(expectedAmount: number): Promise<void> {
  return new Promise((resolve) => {
    const listener = program.addEventListener(
      "TokensMintedEvent",
      (event, slot) => {
        if (event.amount.toNumber() === expectedAmount) {
          console.log("Found matching mint event!");
          program.removeEventListener(listener);
          resolve();
        }
      }
    );
  });
}

// Use it
await program.methods.mintTokens(new BN(1000)).rpc();
await waitForMintEvent(1000);
console.log("Mint confirmed via event!");

Event Filtering

Filter by Field Value

const listener = program.addEventListener(
  "TreasuryDepositEvent",
  (event, slot) => {
    // Only process large deposits
    if (event.amount.toNumber() >= 1_000_000) {
      console.log("Large deposit detected:", event.amount.toString());
      notifyAdmin(event);
    }
  }
);

Filter by Multiple Conditions

const listener = program.addEventListener(
  "RoleAssignedEvent",
  (event, slot) => {
    // Only admin role assignments by specific user
    if (
      event.roleType.admin &&
      event.assignedBy.equals(specificAdmin)
    ) {
      console.log("Admin role assigned by authorized user");
    }
  }
);

Testing Events

Assert Event Was Emitted

import { expect } from "chai";

it("Should emit TokensMintedEvent", async () => {
  let eventEmitted = false;
  let eventData: any = null;

  const listener = program.addEventListener(
    "TokensMintedEvent",
    (event, slot) => {
      eventEmitted = true;
      eventData = event;
    }
  );

  // Execute instruction
  await program.methods
    .mintTokens(new BN(1000))
    .accounts({
      signer: user.publicKey,
      mint: mintPda,
      tokenAccount: userTokenAccount,
      mintAuthority: mintAuthority,
      tokenProgram: TOKEN_PROGRAM_ID,
      associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
      systemProgram: SystemProgram.programId,
    })
    .signers([user])
    .rpc();

  // Wait for event
  await new Promise((resolve) => setTimeout(resolve, 1000));

  // Verify event
  expect(eventEmitted).to.be.true;
  expect(eventData.amount.toNumber()).to.equal(1000);
  expect(eventData.mint.toString()).to.equal(mintPda.toString());

  await program.removeEventListener(listener);
});

Test Event Data

it("Should emit correct deposit event data", async () => {
  const depositAmount = 5_000_000; // 0.005 SOL
  let capturedEvent: any = null;

  const listener = program.addEventListener(
    "TreasuryDepositEvent",
    (event) => {
      capturedEvent = event;
    }
  );

  await program.methods
    .depositToTreasury(new BN(depositAmount))
    .accounts({...})
    .rpc();

  await new Promise((resolve) => setTimeout(resolve, 1000));

  expect(capturedEvent).to.not.be.null;
  expect(capturedEvent.amount.toNumber()).to.equal(depositAmount);
  expect(capturedEvent.depositor.toString()).to.equal(depositor.publicKey.toString());
  expect(capturedEvent.totalDeposited.toNumber()).to.be.greaterThan(0);

  await program.removeEventListener(listener);
});

Best Practices

✅ Do

  • Include timestamps - Makes events easier to track
  • Add context - Include all relevant pubkeys and amounts
  • Emit after success - Only emit when operation completes
  • Use descriptive names - TokensMintedEvent not Event1
  • Keep events focused - One event per significant action

❌ Don’t

  • Emit before validation - May emit then fail
  • Include sensitive data - Events are public
  • Emit too frequently - Each emit costs compute units
  • Forget to remove listeners - Memory leaks in client

Event vs Account Storage

Use Case Use Events Use Accounts
Notify clients ✅ ❌
Historical logging ✅ ❌
Temporary data ✅ ❌
Query by indexer ✅ ✅
Permanent storage ❌ ✅
Complex queries ❌ ✅
State that programs read ❌ ✅

Rule of thumb: Use events for notifications, use accounts for state.


Advanced: Custom Event Parsing

import { BorshCoder, EventParser } from "@coral-xyz/anchor";

// Parse events from transaction logs
const coder = new BorshCoder(program.idl);
const eventParser = new EventParser(program.programId, coder);

const tx = await connection.getTransaction(signature, {
  commitment: "confirmed",
});

const events = eventParser.parseLogs(tx.meta.logMessages);

for (let event of events) {
  console.log("Event:", event.name);
  console.log("Data:", event.data);
}

Next: SPL Token Operations →