Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/blueshift-gg/quasar/llms.txt

Use this file to discover all available pages before exploring further.

On-chain events let off-chain indexers, clients, and other programs observe what happened inside a transaction. Quasar provides two emission strategies with different cost and trust tradeoffs: a fast log-based path via emit! and a spoofing-resistant self-CPI path via emit_cpi!.

Defining Event Types with #[event]

Annotate a struct with #[event(discriminator = N)] to declare an event type. The macro generates an Event trait impl with a discriminator, a fixed data size, and the serialization logic for both emission paths:
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,
}
Event fields follow the same layout rules as #[account] fields: every type must be a Pod type with alignment 1 (Address, u64, u32, u8, bool, etc.).

The Event Trait

The generated impl satisfies the Event trait from quasar_lang::traits:
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>;
}

emit! — Log-Based Emission (~100 CU)

emit!(MyEvent { ... }) serializes the event into a stack buffer and calls sol_log_data. This is the fastest emission path (~100 CU) and produces a Program data: ... log entry that off-chain indexers can parse.
// From the escrow make instruction
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(())
}
// Simple single-field events
emit!(RefundEvent {
    escrow: *self.escrow.address(),
});
Caveat: sol_log_data logs are not linked to a specific program in the transaction trace. Any program in the same transaction can call sol_log_data with the same bytes, making emit! events spoofable by a malicious program that runs in the same transaction.

emit_cpi! — Self-CPI Emission (~1,000 CU)

emit_cpi!(MyEvent { ... }) performs a self-CPI to the program’s own __event_authority PDA. Because the Solana runtime records which program made each CPI call, the emitting program’s address appears in the transaction trace alongside the event — making the event unforgeable.
// In a handler where the accounts struct contains program and event_authority:
emit_cpi!(MakeEvent {
    escrow: *self.escrow.address(),
    // ...
});
This costs ~1,000 CU (one CPI invocation) vs ~100 CU for emit!.
Use when:
  • Off-chain indexers are the only consumers
  • You trust the transaction context (e.g. your own frontends)
  • Compute budget is tight
  • Spoofability is acceptable for your use case
emit!(TakeEvent {
    escrow: *self.escrow.address(),
});

The __event_authority PDA

The #[program] macro generates a constant EventAuthority type. Its BUMP field is the canonical bump for the __event_authority PDA derived from b"__event_authority" and the program ID:
// Generated by #[program]
pub struct EventAuthority;
impl EventAuthority {
    pub const BUMP: u8 = /* derived at compile time */;
}
When emit_cpi! runs, it signs the self-CPI with seeds [b"__event_authority", &[BUMP]]. The __handle_event dispatch arm validates:
  1. At least one account was passed
  2. The first account is a signer
  3. Its address matches the __event_authority PDA
  4. The instruction data is more than 1 byte (the 0xFF CPI marker plus payload)
Then it calls sol_log_data with the remaining bytes (discriminator + event data).

Event Serialization Format

Both emission paths serialize events the same way on the wire:
emit!:      [discriminator bytes...] [event data bytes...]
emit_cpi!:  [0xFF] [discriminator bytes...] [event data bytes...]
The 0xFF prefix distinguishes self-CPI invocations from normal instruction calls in the dispatch router.

Reading Events Off-Chain

Events emitted via emit! appear in the transaction logs as Program data: <base64>. Parse them by:
  1. Filtering Program data: log entries for your program’s logs
  2. Base64-decoding the payload
  3. Matching the first byte(s) against known discriminators
  4. Deserializing the remaining bytes according to the event layout
The generated client SDK (from quasar build --client) handles this automatically. For manual parsing, see the escrow client’s event decoder:
pub fn decode_event(data: &[u8]) -> Option<ProgramEvent> {
    if data.starts_with(MAKE_EVENT_DISCRIMINATOR) {
        return wincode::deserialize::<MakeEvent>(data)
            .ok()
            .map(ProgramEvent::MakeEvent);
    }
    if data.starts_with(TAKE_EVENT_DISCRIMINATOR) {
        return wincode::deserialize::<TakeEvent>(data)
            .ok()
            .map(ProgramEvent::TakeEvent);
    }
    if data.starts_with(REFUND_EVENT_DISCRIMINATOR) {
        return wincode::deserialize::<RefundEvent>(data)
            .ok()
            .map(ProgramEvent::RefundEvent);
    }
    None
}
MAKE_EVENT_DISCRIMINATOR is the constant &[u8] matching the #[event(discriminator = 0)] value.
The event discriminator namespace is separate from the account and instruction discriminator namespaces. You may safely use discriminator = 0 on an event type even if discriminator = 0 is already used on an instruction in the same program.

Build docs developers (and LLMs) love