Quasar
References

Type Mapping

Mapping between Rust types, zero-copy Pod representations, and IDL/TypeScript types.

Quasar reads account data directly from the runtime's memory buffer without deserialization. Native Rust integer types are mapped to alignment-1 Pod types that store values as little-endian byte arrays.

Rust to Pod type mapping

When you define an #[account] struct, the derive macro generates a #[repr(C)] companion struct where each field is mapped to its zero-copy equivalent:

Rust TypeZero-Copy TypeSize (bytes)Alignment
u8u8 (no wrapper)11
i8i8 (no wrapper)11
u16PodU1621
i16PodI1621
u32PodU3241
i32PodI3241
u64PodU6481
i64PodI6481
u128PodU128161
i128PodI128161
boolPodBool11
AddressAddress (no wrapper)321

All Pod types have alignment 1. The Solana runtime provides account data in a flat byte buffer with no alignment guarantees, so all types in #[repr(C)] account structs must have alignment 1.

Given this account definition:

#[account(discriminator = 1)]
pub struct Vault {
    pub authority: Address,
    pub balance: u64,
    pub is_active: bool,
}

The derive macro generates this zero-copy companion:

#[repr(C)]
#[derive(Copy, Clone)]
pub struct VaultZc {
    pub authority: Address,     // 32 bytes
    pub balance: PodU64,        // 8 bytes
    pub is_active: PodBool,     // 1 byte
}

Alignment is verified at compile time:

const _: () = assert!(core::mem::align_of::<VaultZc>() == 1);

Pod Types

Pod types are defined in quasar_pod and re-exported via quasar_lang::prelude. Each is #[repr(transparent)] over [u8; N], storing values in little-endian byte order.

Reading values

Use .get() to extract the native Rust value:

let balance: u64 = vault.balance.get();
let is_active: bool = vault.is_active.get();
let slot: u64 = clock.slot.get();

Writing values

Assign native values directly. The #[account] macro generates setters that handle the conversion:

// Direct field access via DerefMut
vault.balance = PodU64::from(1000u64);
vault.is_active = PodBool::from(true);

Arithmetic

Pod types implement +, -, *, /, % with both Pod and native types:

let a = PodU64::from(100u64);
let b = a + 50u64;           // PodU64 + u64 -> PodU64
let c = a + PodU64::from(50); // PodU64 + PodU64 -> PodU64
let d = a * 2u64;            // PodU64 * u64 -> PodU64

Overflow behavior:

  • Debug builds: Panics on overflow (via checked_* operations)
  • Release builds: Uses wrapping semantics for CU efficiency

For explicit overflow detection in all builds:

MethodDescription
checked_add(rhs)Returns None on overflow
checked_sub(rhs)Returns None on underflow
checked_mul(rhs)Returns None on overflow
checked_div(rhs)Returns None if rhs is zero
saturating_add(rhs)Clamps at numeric bounds
saturating_sub(rhs)Clamps at zero (unsigned) or bounds (signed)
saturating_mul(rhs)Clamps at numeric bounds
let balance = vault.balance;
let new_balance = balance.checked_add(deposit_amount)
    .ok_or(ProgramError::ArithmeticOverflow)?;

Constants

ConstantDescription
ZEROThe zero value
MAXThe largest representable value
MINThe smallest representable value
if vault.balance == PodU64::ZERO {
    return Err(MyError::EmptyVault.into());
}

Comparisons

Pod types implement PartialEq, Eq, PartialOrd, and Ord. Compare directly with native types:

if vault.balance > 100u64 {
    // ...
}
if vault.balance == PodU64::ZERO {
    // ...
}

PodBool

Stores a boolean as a single byte. Any non-zero byte is true (canonical form is 0x01).

let active: bool = vault.is_active.get();  // read
vault.is_active = PodBool::from(true);     // write
let toggled = !vault.is_active;            // bitwise NOT

Bitwise operations

Pod integer types support:

OperatorDescription
&Bitwise AND
|Bitwise OR
^Bitwise XOR
!Bitwise NOT
<<Left shift
>>Right shift

Discriminator

Every account and instruction type has a discriminator prefix -- a byte sequence at the start of account data that identifies the type.

Account discriminators

Set via #[account]:

#[account(discriminator = 1)]
pub struct Vault { ... }

#[account(discriminator = [1, 2])]
pub struct Escrow { ... }

Rules:

  • At least one byte is required
  • At least one byte must be non-zero (all-zero discriminators are rejected at compile time, since they match uninitialized data)
  • Single values use a one-byte discriminator; arrays support multi-byte discriminators

Instruction discriminators

Set via #[instruction]:

#[instruction(discriminator = 0)]
pub fn initialize(ctx: Ctx<Initialize>) -> Result<(), ProgramError> { ... }

#[instruction(discriminator = 1)]
pub fn deposit(ctx: Ctx<Deposit>) -> Result<(), ProgramError> { ... }

Instruction discriminators can be zero. The quasar new instruction command auto-increments from the highest existing discriminator.


Calculating account space

Total account space:

space = discriminator_size + fixed_fields_size + dynamic_prefix_bytes + dynamic_data_bytes

Fixed-size accounts

For accounts with only fixed-width fields, Space provides a compile-time constant:

#[account(discriminator = 1)]
pub struct Vault {
    pub authority: Address,  // 32 bytes
    pub balance: u64,        // 8 bytes
    pub is_active: bool,     // 1 byte
}
// Vault::SPACE = 1 (disc) + 32 + 8 + 1 = 42 bytes

The formula is SPACE = discriminator_bytes + sizeof(ZcCompanion), where sizeof(ZcCompanion) is the sum of all field sizes after Pod mapping:

FieldPod TypeSize
authority: AddressAddress32
balance: u64PodU648
is_active: boolPodBool1
Total fields41
+ discriminator (1 byte)42

Dynamic accounts

For accounts with String or Vec fields, space is variable. The macro provides MIN_SPACE and MAX_SPACE:

#[account(discriminator = 5)]
pub struct Profile<'a> {
    pub owner: Address,              // 32 bytes (fixed)
    pub name: String<u16, 64>,       // 2 prefix + 0..64 data
    pub tags: Vec<Address, u8, 10>,  // 1 prefix + 0..320 data
}
// Profile::MIN_SPACE = 1 (disc) + 32 (fixed) + 2 + 1 = 36 bytes
// Profile::MAX_SPACE = 36 + 64 + 320 = 420 bytes

Dynamic field layout:

Field TypePrefixData Bytes
Stringu32 (4 bytes)0..1024
String<u16, N>u16 (2 bytes)0..N
String<u8, N>u8 (1 byte)0..N
Vec<T>u32 (4 bytes)0..8 * sizeof(T)
Vec<T, u16, N>u16 (2 bytes)0..N * sizeof(T)
Vec<T, u8, N>u8 (1 byte)0..N * sizeof(T)
&str (tail)noneremaining bytes
&[u8] (tail)noneremaining bytes

String and Vec are marker types that configure the wire format:

// String<P, MAX> where P = prefix type, MAX = max byte length
pub name: String,             // String<u32, 1024> (defaults)
pub tag: String<u8, 32>,     // u8 prefix, max 32 bytes
pub bio: String<u16, 4096>,  // u16 prefix, max 4096 bytes

// Vec<T, P, MAX> where T = element, P = prefix type, MAX = max count
pub members: Vec<Address>,           // Vec<Address, u32, 8> (defaults)
pub scores: Vec<PodU64, u8, 4>,     // u8 prefix, max 4 elements

Tail fields

A tail field (&str or &[u8]) consumes all remaining bytes with no length prefix. Must be the last dynamic field. Maximum size is 1024 bytes.

#[account(discriminator = 5)]
pub struct Note<'a> {
    pub author: Address,
    pub content: &'a str,  // tail field -- no prefix, uses remaining data
}

Space override

Use space = <expr> on init to override computed space:

#[account(init, payer = payer, space = 8 + 32 + 4 + name.len())]
pub profile: &'info mut Account<Profile<'info>>,

IDL type mapping

The IDL generator maps Rust types to IDL primitives, which the TypeScript client generator maps to TypeScript types and codecs.

Rust to IDL to TypeScript

Rust TypeIDL TypeTypeScript TypeCodec (@solana/codecs)
u8u8numbergetU8Codec()
u16u16numbergetU16Codec()
u32u32numbergetU32Codec()
u64u64bigintgetU64Codec()
u128u128bigintgetU128Codec()
i8i8numbergetI8Codec()
i16i16numbergetI16Codec()
i32i32numbergetI32Codec()
i64i64bigintgetI64Codec()
i128i128bigintgetI128Codec()
boolboolbooleangetBooleanCodec()
Address / PubkeypublicKeyAddressgetAddressCodec() (kit)
Stringstringstring--
String<N>{ string: { maxLength: N } }stringgetDynStringCodec()
Vec<T, N>{ vec: { items: T, maxLength: N } }Array<T>getDynVecCodec(T)
&str (tail){ tail: { element: "string" } }stringgetUtf8Codec()
&[u8] (tail){ tail: { element: "bytes" } }Uint8ArraygetBytesCodec()
Custom struct{ defined: "Name" }NameNameCodec

On this page