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.

Solana instructions can pass more accounts than a program’s declared struct accounts. The accounts after the declared set are called “remaining accounts.” Quasar provides zero-allocation access to these trailing accounts through RemainingAccounts, a boundary-pointer-based iterator that walks the SVM input buffer directly. For typed access, Remaining<T, N> parses trailing accounts into a fixed-capacity array of typed wrappers.

Ctx vs CtxWithRemaining

Two instruction context types exist depending on whether you need remaining accounts:
// Use when you do NOT need remaining accounts
pub struct Ctx<'input, T> {
    pub accounts: T,
    pub bumps: T::Bumps,
    pub program_id: &'input [u8; 32],
    pub data: &'input [u8],
}

// Use when you DO need remaining accounts
pub struct CtxWithRemaining<'input, T> {
    pub accounts: T,
    pub bumps: T::Bumps,
    pub program_id: &'input [u8; 32],
    pub data: &'input [u8],
    // remaining_ptr, declared, accounts_boundary are private
}
CtxWithRemaining preserves the raw buffer pointer past the declared accounts region. Call ctx.remaining_accounts() to get a RemainingAccounts accessor:
pub fn handler(ctx: CtxWithRemaining<MyAccounts>) -> Result<(), ProgramError> {
    let remaining = ctx.remaining_accounts();
    // ...
    Ok(())
}

RemainingAccounts

RemainingAccounts is a zero-allocation accessor over the trailing SVM account buffer. It stores a ptr (start of first remaining account) and boundary (end of accounts region) rather than a count, so there are no reads in the dispatch hot path:
pub struct RemainingAccounts<'a> {
    ptr: *mut u8,
    boundary: *const u8,
    declared: &'a [AccountView],
    program_id: Option<&'a Address>,
    data: &'a [u8],
}

RemainingAccounts Methods

// Check if no remaining accounts remain
let empty: bool = remaining.is_empty();

// Random-access by index (O(n) walk from buffer start)
let account: Option<RemainingAccount> = remaining.get(0)?;

// Sequential iterator (O(1) dup resolution, 64-account limit)
for account in remaining.iter() {
    let account = account?;
    // ...
}

// Parse into a typed Remaining<T, N>
let typed: Remaining<Account<MyType>, 10> = remaining.parse::<Account<MyType>, 10>()?;

RemainingAccount

RemainingAccount is a thin wrapper around AccountView for individual remaining accounts. It exposes safe accessors without exposing raw unchecked data pointers:
// Safe accessors on RemainingAccount
let addr: &Address = account.address();
let signer: bool    = account.is_signer();
let writable: bool  = account.is_writable();
let owner: &Address = account.owner();
let executable: bool = account.executable();
let lamports: u64   = account.lamports();
let data_len: usize = account.data_len();

// Safe borrow of account data (uses runtime borrow state)
let data = account.try_borrow_data()?;
let data_mut = account.try_borrow_data_mut()?;
Duplicate account entries in the remaining buffer are resolved to their canonical runtime account, so borrow-state safety is maintained even when the same physical account appears multiple times.

Remaining<T, N>

Remaining<T, N> parses trailing accounts into a fixed-capacity stack-allocated array of up to N items of type T. Each T must implement RemainingItem:
pub struct Remaining<T, const N: usize> {
    items: [MaybeUninit<T>; N],
    len: usize,
}

Supported T Types

All major Quasar account wrappers implement RemainingItem:
TypeNotes
Account<T>Validates owner + discriminator + data length
InterfaceAccount<T>Validates against T::owners()
Program<T>Validates executable + address
Interface<T>Validates executable + T::matches(address)
Sysvar<T>Validates sysvar address
SignerValidates is_signer; allows duplicates
UncheckedAccountNo validation

Parsing Typed Remaining Accounts

pub fn handler(ctx: CtxWithRemaining<MyAccounts>) -> Result<(), ProgramError> {
    let remaining = ctx.remaining_accounts();

    // Parse up to 8 Account<Voter> from remaining accounts
    let voters: Remaining<Account<Voter>, 8> = remaining.parse::<Account<Voter>, 8>()?;

    for voter in voters.iter() {
        let vote_weight: u64 = voter.weight.get();
        // ...
    }

    Ok(())
}

Remaining Methods

// As a slice of T
let slice: &[T] = remaining.as_slice();

// Iterator
for item in remaining.iter() { /* ... */ }

// Capacity and length
let len: usize = remaining.len();
let cap: usize = remaining.capacity();  // always N
let empty: bool = remaining.is_empty();
The maximum number of remaining accounts that can be iterated via RemainingAccounts::iter() is 64 (MAX_REMAINING_ACCOUNTS). If more than 64 accounts are present and the iterator is used, it returns Err(QuasarError::RemainingAccountsOverflow). Use Remaining<T, N> with a known upper bound instead when the account count is bounded.

Duplicate Account Handling

Remaining accounts may include duplicate entries — entries that point back to an earlier account (declared or already-seen remaining) rather than carrying a full account header. Quasar resolves these in two ways:
  • RemainingAccounts::iter(): builds a cache of yielded views as it walks; duplicate resolution is O(1) by index lookup into the declared slice or the cache.
  • RemainingAccounts::get(index): performs a linear walk to resolve the entry at index; duplicates are followed via up to 2-hop chain resolution.
For Remaining<T, N>, duplicates are rejected by default (REJECT_DUPLICATES = true). Only Signer sets REJECT_DUPLICATES = false, allowing it to appear in both the declared accounts and the remaining accounts without triggering a duplicate error:
// Signer allows duplicates (a signer may appear in both declared and remaining)
impl<'input> RemainingItem<'input> for Signer {
    const REJECT_DUPLICATES: bool = false;
    // ...
}

// All others default to true (including UncheckedAccount, Account<T>, etc.)
// impl RemainingItem for Account<T> { const REJECT_DUPLICATES: bool = true; ... }

RemainingItem Trait

To make a custom type work as a remaining account item, implement RemainingItem:
pub trait RemainingItem<'input>: Sized {
    /// How many raw remaining-account slots this item consumes.
    const COUNT: usize;

    /// Whether duplicate addresses are rejected.
    const REJECT_DUPLICATES: bool = true;

    /// Parse one item from a single AccountView.
    unsafe fn parse_remaining_one(
        account: AccountView,
        program_id: Option<&Address>,
        data: &[u8],
    ) -> Result<Self, ProgramError>;

    /// Parse one item from a slice of exactly COUNT AccountViews.
    unsafe fn parse_remaining_chunk(
        accounts: &'input mut [AccountView],
        program_id: Option<&Address>,
        data: &[u8],
    ) -> Result<Self, ProgramError>;
}
For COUNT > 1, the item consumes multiple raw accounts per parse step. This enables typed #[derive(Accounts)] groups to be used as remaining-account chunks.

Complete Example: Multivote with Remaining Signers

use quasar_lang::prelude::*;

#[derive(Accounts)]
pub struct BatchVote {
    #[account(mut)]
    pub proposal: Account<Proposal>,
}

pub fn handler(ctx: CtxWithRemaining<BatchVote>) -> Result<(), ProgramError> {
    let remaining = ctx.remaining_accounts();

    // Parse up to 16 signers from remaining accounts
    let signers: Remaining<Signer, 16> = remaining.parse::<Signer, 16>()?;

    for signer in signers.iter() {
        let key: &Address = signer.address();
        // Record each vote...
        ctx.accounts.proposal.record_vote(key)?;
    }

    Ok(())
}
Remaining accounts are not validated by the #[derive(Accounts)] struct. Any security properties (ownership, signer status, address) must be enforced either by the typed Remaining<T, N> wrapper (which runs each T’s AccountLoad checks) or by explicit manual validation in your handler when using raw RemainingAccount.

Build docs developers (and LLMs) love