Program Structure
The #[program] macro, module layout, allocator configuration, and debug mode.
The #[program] macro generates the on-chain entrypoint, instruction dispatch, event authority PDA, and allocator configuration.
Module Layout
A typical Quasar program:
my-program/
Cargo.toml
src/
lib.rs # Program entry, declare_id!, #[program] module
instructions/ # One file per instruction handler
mod.rs
deposit.rs
withdraw.rs
state.rs # #[account] definitions
errors.rs # #[error_code] enum
events.rs # #[event] definitionsExample lib.rs from the escrow program:
#![no_std]
use quasar_lang::prelude::*;
mod instructions;
use instructions::*;
mod events;
mod state;
declare_id!("22222222222222222222222222222222222222222222");
#[program]
mod quasar_escrow {
use super::*;
#[instruction(discriminator = 0)]
pub fn make(ctx: Ctx<Make>, deposit: u64, receive: u64) -> Result<(), ProgramError> {
ctx.accounts.make_escrow(receive, &ctx.bumps)?;
ctx.accounts.emit_event(deposit, receive)?;
ctx.accounts.deposit_tokens(deposit)
}
#[instruction(discriminator = 1)]
pub fn take(ctx: Ctx<Take>) -> Result<(), ProgramError> {
ctx.accounts.transfer_tokens()?;
ctx.accounts.withdraw_tokens_and_close(&ctx.bumps)?;
ctx.accounts.emit_event()
}
#[instruction(discriminator = 2)]
pub fn refund(ctx: Ctx<Refund>) -> Result<(), ProgramError> {
ctx.accounts.withdraw_tokens_and_close(&ctx.bumps)?;
ctx.accounts.emit_event()
}
}#![no_std]is required. Quasar programs do not linkstd.declare_id!sets thecrate::IDconstant used for owner checks and PDA derivation.#[program]annotates a module containing#[instruction]functions. The module name (quasar_escrow) determines the generated program type name (QuasarEscrow). Aclientsubmodule with typed instruction builders is also generated (compiled only for off-chain targets).
What #[program] Generates
Entrypoint
The entry function that the runtime calls when your program is invoked. Initializes the allocator (if alloc is enabled) and dispatches to the matched instruction handler.
Dispatch table
Reads the discriminator prefix from instruction data and matches it against a compile-time table of handlers. 0xFF prefix routes to the self-CPI event handler.
Compile-time rules:
- All instruction discriminators must have the same byte length.
- No duplicates.
- No discriminator may start with
0xFF(reserved for events).
Program type and event authority
- A program type (e.g.,
QuasarEscrow) that validates the executable flag and address. Implements theIdtrait. - An
EventAuthorityPDA (seeds:["__event_authority"]) used byemit_cpi!to sign self-CPI event emissions.
Panic handler
A minimal panic handler that logs "PANIC" on-chain.
Allocator Configuration
By default, #[program] installs a global allocator that panics on any allocation attempt. Most programs never need heap allocation because Quasar provides zero-copy alternatives for dynamic data:
String<'a, MAX> and Vec<'a, T, MAX> -- stored inline in account data with a length prefix, accessed as &str and &[T] slices. No allocation, no deserialization:
#[account(discriminator = 1)]
pub struct MultisigConfig<'a> {
pub creator: Address,
pub threshold: u8,
pub label: String<'a, 32>, // max 32 bytes
pub signers: Vec<'a, Address, 10>, // max 10 elements
}If you only have one dynamic field and it's the last field in the struct, use a bare &'a str or &'a [u8] instead. This avoids the length prefix entirely -- the field consumes all remaining bytes in the account, which is slightly more efficient:
#[account(discriminator = 1)]
pub struct Note<'a> {
pub author: Address,
pub content: &'a str, // tail field -- no prefix, uses remaining space
}If your program genuinely requires heap allocation, enable the alloc feature:
[dependencies]
quasar-lang = { version = "...", features = ["alloc"] }With alloc enabled, a bump allocator is installed instead.
Debug Mode
Build with debug to enable detailed error logging:
quasar build --debugValidation failures produce log messages like:
Account 'escrow' (index 1): must be writable, no duplicatesWithout --debug, these log statements are compiled out. Always build without --debug for production deployments.
Instruction Handler Pattern
Each handler takes ctx: Ctx<T> (or ctx: CtxWithRemaining<T>) as its first parameter, plus any typed arguments, and returns Result<(), ProgramError>. Business logic goes in impl blocks on the accounts struct:
#[instruction(discriminator = 0)]
pub fn deposit(ctx: Ctx<Deposit>, amount: u64) -> Result<(), ProgramError> {
ctx.accounts.deposit(amount)
}For instructions that forward a variable number of accounts to CPIs, use CtxWithRemaining:
#[instruction(discriminator = 0)]
pub fn create(ctx: CtxWithRemaining<Create>, threshold: u8) -> Result<(), ProgramError> {
ctx.accounts.create_multisig(threshold, &ctx.bumps, ctx.remaining_accounts())
}Next Steps
- Accounts and Validation -- how account types and
#[derive(Accounts)]work - Instructions -- the
#[instruction]macro and argument handling - IDL -- generating the program interface description
