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:
| Type | Notes |
|---|
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 |
Signer | Validates is_signer; allows duplicates |
UncheckedAccount | No 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.