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. PubkeyString- 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 -
TokensMintedEventnotEvent1 - 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 →