Quasar
Features

Events

Emitting on-chain events via log-based and self-CPI mechanisms.

Two emission strategies with different cost and security tradeoffs:

  • emit!() -- emits via sol_log_data at ~100 CU. Fast but spoofable: any program can emit logs that look like another program's events.
  • emit_cpi!() -- emits via self-CPI at ~1,000 CU. The program's ID appears in the transaction trace, proving the event originated from the program.

Both use the same event struct definition. You choose the method at the call site.

Defining events

Use #[event] on a struct with named fields. Each event requires an explicit discriminator:

use quasar_lang::prelude::*;

#[event(discriminator = 0)]
pub struct MakeEvent {
    pub escrow: Address,
    pub maker: Address,
    pub mint_a: Address,
    pub mint_b: Address,
    pub deposit: u64,
    pub receive: u64,
}

#[event(discriminator = 1)]
pub struct TakeEvent {
    pub escrow: Address,
}

#[event(discriminator = 2)]
pub struct RefundEvent {
    pub escrow: Address,
}

Each event needs a unique discriminator within the program, used to identify the event type when parsing logs.

Supported field types

TypeSize
u8, i8, bool1 byte
u16, i162 bytes
u32, i324 bytes
u64, i648 bytes
u128, i12816 bytes
Address32 bytes

Strings, vectors, and custom structs are not supported. The macro rejects unsupported types at compile time.

What the macro generates

The #[event] macro generates a #[repr(C)] layout, a compile-time no-padding assertion, and an Event trait implementation:

pub trait Event {
    const DISCRIMINATOR: &'static [u8];
    const DATA_SIZE: usize;
    fn write_data(&self, buf: &mut [u8]);
    fn emit(&self, f: impl FnOnce(&[u8]) -> Result<(), ProgramError>) -> Result<(), ProgramError>;
}

Log-based emission with emit!()

emit!() calls sol_log_data to write event data to the transaction log (~100 CU):

impl<'info> Make<'info> {
    #[inline(always)]
    pub fn emit_event(&self, deposit: u64, receive: u64) -> Result<(), ProgramError> {
        emit!(MakeEvent {
            escrow: *self.escrow.address(),
            maker: *self.maker.address(),
            mint_a: *self.mint_a.address(),
            mint_b: *self.mint_b.address(),
            deposit,
            receive,
        });
        Ok(())
    }
}

Wire format: [discriminator bytes] [struct data].

When to use emit!()

  • CU budget is tight (~100 CU).
  • The event consumer trusts the transaction's program invocation chain (e.g., your own indexer).
  • Spoofing resistance is not required.

Self-CPI emission with emit_cpi!()

emit_cpi!() emits events via a self-CPI -- the program invokes itself with the event data as instruction data. The program's ID appears in the instruction trace, so indexers can verify the event origin.

The Event Authority PDA

Self-CPI events require an Event Authority PDA derived from the seed "__event_authority". The #[program] macro generates this automatically:

// Generated by #[program]
pub struct EventAuthority;

impl EventAuthority {
    pub const BUMP: u8 = /* computed at compile time or init */;
}

To use emit_cpi!(), your instruction's Accounts struct must include both the program account and the event authority:

#[derive(Accounts)]
pub struct EmitViaCpi<'info> {
    pub signer: &'info Signer,
    pub event_authority: &'info EventAuthority,
    pub program: &'info Program<QuasarTestEvents>,
}

Using emit_cpi!()

The macro must be called inside an instruction handler with access to self.program and self.event_authority:

impl<'info> EmitViaCpi<'info> {
    #[inline(always)]
    pub fn handler(&self, value: u64) -> Result<(), ProgramError> {
        emit_cpi!(SimpleEvent { value })?;
        Ok(())
    }
}

The macro constructs event data with a 0xFF prefix byte, creates a CPI with the event authority as a readonly signer, signs with the PDA seeds ["__event_authority", &[bump]], and invokes the program itself.

Wire format: [0xFF] [discriminator bytes] [struct data].

The 0xFF prefix is reserved -- a compile-time assertion ensures no instruction discriminator equals 0xFF, preventing collisions with regular instruction dispatch.

When to use emit_cpi!()

  • You need spoofing resistance: indexers must verify the event was emitted by your program, not a malicious program that forged log data.
  • The ~1,000 CU cost is acceptable.
  • The instruction already has the event authority and program accounts available.

Cost comparison

MethodApproximate CUSpoofing resistantAccounts required
emit!()~100 CUNoNone
emit_cpi!()~1,000 CUYesEventAuthority, Program<Self>

Discriminator management

Each event must have a unique discriminator within a program:

#[event(discriminator = 0)]
pub struct MakeEvent { ... }

#[event(discriminator = 1)]
pub struct TakeEvent { ... }

#[event(discriminator = 2)]
pub struct RefundEvent { ... }

Discriminators can be single bytes or multi-byte arrays. Single-byte discriminators are the common case.

Event discriminators occupy a separate namespace from instruction and account discriminators -- discriminator = 0 on an event does not conflict with discriminator = 0 on an instruction.

Empty events

Events with no fields are supported -- they emit only the discriminator, useful as a signal that an action occurred:

#[event(discriminator = 5)]
pub struct EmptyEvent {}

Parsing events off-chain

emit!() events appear as base64-encoded sol_log_data entries: [discriminator: N bytes] [data: DATA_SIZE bytes].

emit_cpi!() events appear as instruction data in the transaction's inner instructions: [0xFF] [discriminator: N bytes] [data: DATA_SIZE bytes].

To decode: read the discriminator bytes to identify the event type, then cast the remaining bytes to the event's #[repr(C)] struct (little-endian integers).

The IDL includes event definitions with field types and discriminators, enabling client libraries to generate parsers automatically.

On this page