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.

Cross-program invocations (CPIs) let your program call instructions on other Solana programs — the System Program to create accounts, SPL Token to transfer tokens, or your own program for self-CPI events. Quasar provides two stack-only CPI builders that go directly to the sol_invoke_signed_c syscall without heap allocation: CpiCall for calls with a fixed account count and CpiDynamic for variable-length calls.

CpiCall<N, D> — Const-Generic Fixed Builder

CpiCall is parameterized by the number of accounts N and the byte length of instruction data D. Both are known at compile time, so the entire struct lives on the stack:
pub struct CpiCall<'a, const ACCTS: usize, const DATA: usize> {
    program_id: &'a Address,
    accounts: [InstructionAccount<'a>; ACCTS],
    cpi_accounts: [CpiAccount<'a>; ACCTS],
    data: [u8; DATA],
}
This is the type returned by all system and SPL Token CPI helpers. You typically never construct it manually — call the helper and chain an invocation method:
// System transfer: CpiCall<2, 12>
system::transfer(from, to, lamports).invoke()?;

// SPL Token transfer: returned by TokenProgram helper
self.token_program
    .transfer(&self.maker_ta_a, &self.vault_ta_a, &self.maker, amount)
    .invoke()?;

Invocation Methods

MethodWhen to use
.invoke()No PDA signers
.invoke_signed(&seeds)One PDA signer
.invoke_with_signers(&[Signer])Multiple PDA signers
.invoke_with_return()No signers, read return data
.invoke_signed_with_return(&seeds)One signer, read return data

CpiDynamic<MAX_N, MAX_D> — Runtime-Length Builder

When the account count or data size varies at runtime but is bounded by compile-time constants, use CpiDynamic. Accounts are pushed one at a time; data is set all at once. A compile-time assertion prevents the struct from exceeding the SVM’s 3 KiB safe stack budget:
const _STACK_CHECK: () = assert!(
    56 * MAX_ACCTS + 24 * MAX_ACCTS + MAX_DATA + 24 <= 3072,
    "CpiDynamic exceeds safe 3 KiB stack budget for SVM 4 KiB frames"
);
Usage:
let mut cpi = CpiDynamic::<8, 64>::new(&target_program_id);
cpi.push_account(account, /*is_signer=*/false, /*is_writable=*/true)?;
cpi.set_data(&instruction_data)?;
cpi.invoke()?;

Core CPI Types

All CPI-related types live in quasar_lang::cpi and are re-exported from solana-instruction-view:

InstructionAccount

Per-account metadata for the instruction: address + signer/writable flags. Constructed with helpers: InstructionAccount::writable_signer(addr), InstructionAccount::writable(addr), InstructionAccount::readonly(addr), InstructionAccount::readonly_signer(addr).

CpiAccount

Full account view passed to the runtime during the CPI. Built from an &AccountView by cpi_account_from_view. Each CpiCall maintains a parallel array of these alongside the InstructionAccount metas.

Seed / Signer

Seed wraps a &[u8] seed slice. Signer groups a slice of Seeds into one PDA signer entry. Build them from byte slices: Seed::from(b"escrow" as &[u8]).

CpiSignerSeeds

Trait implemented by [Seed<'_>; N] and [Seed<'_>]. Passed to .invoke_signed() — Quasar wraps the array in a Signer and passes it to the syscall.

System Program CPI

Quasar ships built-in helpers for the most common System Program instructions:
use quasar_lang::cpi::system;

// Create a new account (CpiCall<2, 52>)
system::create_account(from, to, lamports, space, &owner).invoke()?;

// Transfer lamports (CpiCall<2, 12>)
system::transfer(from, to, lamports).invoke()?;

// Assign owner (CpiCall<1, 36>)
system::assign(account, &new_owner).invoke()?;

// Allocate data space (CpiCall<1, 12>)
system::allocate(account, space).invoke()?;
The Program<SystemProgram> wrapper exposes the same helpers as methods, useful when you have the system program account in your accounts struct:
// In accounts struct:
pub system_program: Program<SystemProgram>,

// In handler:
self.system_program
    .transfer(&self.payer, &self.destination, amount)
    .invoke()?;

Handling Pre-funded Accounts

system::init_account handles both fresh accounts (zero lamports) and pre-funded PDAs (non-zero lamports) without the CreateAccount failure mode:
system::init_account(
    payer,
    &mut account,
    lamports,   // rent-exempt minimum
    space,
    &owner,
    signers,    // PDA seeds, or &[] for keypair accounts
)?;

SPL Token CPI

The quasar-spl crate provides zero-copy token account types and typed CPI helpers:
use quasar_spl::prelude::*;

// Transfer tokens (no PDA signer)
self.token_program
    .transfer(&self.maker_ta_a, &self.vault_ta_a, &self.maker, amount)
    .invoke()?;

// Transfer tokens (PDA signer — escrow signs for vault)
self.token_program
    .transfer(
        &self.vault_ta_a,
        &self.maker_ta_a,
        &self.escrow,
        self.vault_ta_a.amount(),
    )
    .invoke_signed(&seeds)?;

// Close a token account
self.token_program
    .close_account(&self.vault_ta_a, &self.maker, &self.escrow)
    .invoke_signed(&seeds)?;
The full escrow refund from the escrow example:
pub fn withdraw_tokens_and_close(&self, bumps: &RefundBumps) -> Result<(), ProgramError> {
    let bump = [bumps.escrow];
    let seeds = [
        Seed::from(b"escrow" as &[u8]),
        Seed::from(self.maker.address().as_ref()),
        Seed::from(bump.as_ref()),
    ];

    self.token_program
        .transfer(
            &self.vault_ta_a,
            &self.maker_ta_a,
            &self.escrow,
            self.vault_ta_a.amount(),
        )
        .invoke_signed(&seeds)?;

    self.token_program
        .close_account(&self.vault_ta_a, &self.maker, &self.escrow)
        .invoke_signed(&seeds)
}

Reading Return Data

After a CPI call that sets return data, use invoke_with_return() or invoke_signed_with_return() to capture and decode the result:
let ret = some_cpi_call.invoke_with_return()?;

// Decode as a primitive type
let amount: u64 = ret.decode::<u64>()?;

// Decode as a custom struct (must implement InstructionArg)
let result: SwapResult = ret.decode::<SwapResult>()?;
invoke_with_return() verifies that ret.program_id() matches the invoked program before returning. CpiReturn::decode<T> then checks that the data length equals size_of::<T::Zc>().

invoke_raw — Direct Syscall

For the lowest-level CPI control, call invoke_raw directly. This bypasses CpiCall’s type-safe wrappers and goes straight to sol_invoke_signed_c:
// Safety: all pointer/length pairs must be valid for the syscall duration.
let result = unsafe {
    invoke_raw(
        program_id,
        instruction_accounts.as_ptr(),
        instruction_accounts.len(),
        data.as_ptr(),
        data.len(),
        cpi_accounts.as_ptr(),
        cpi_accounts.len(),
        signers,
    )
};
result_from_raw(result)?;
invoke_raw is unsafe. Use CpiCall or CpiDynamic in normal program code. Reserve invoke_raw for situations where const-generic or runtime-length builders cannot express the required layout.

Build docs developers (and LLMs) love