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.
quasar-spl gives you direct, zero-copy access to SPL Token mint and token account data and a clean CPI layer for every standard SPL Token instruction. All reads happen in-place against the account’s raw data buffer — no deserialization, no heap allocation. CPI calls are built as stack-allocated CpiCall values and dispatched with .invoke() or .invoke_signed().
Zero-Copy Account Wrappers
Mint — Account<Mint>
Mint wraps an 82-byte SPL Token mint account. The underlying zero-copy view type is MintDataZc. Every field is available through a generated accessor:
| Field | Type | Description |
|---|
mint_authority() | Option<&Address> | The authority that can mint new tokens, if any |
supply() | u64 | Total token supply (in base units) |
decimals() | u8 | Number of decimal places |
is_initialized() | bool | Whether the mint has been initialized |
freeze_authority() | Option<&Address> | Authority that can freeze token accounts, if any |
// Read mint fields directly — no copy
let supply = self.mint.supply();
let decimals = self.mint.decimals();
let is_init = self.mint.is_initialized();
The compile-time assertion in the crate guarantees the layout is exactly 82 bytes:
const _: () = assert!(core::mem::size_of::<MintDataZc>() == 82);
Token — Account<Token>
Token wraps a 165-byte SPL Token account. The underlying zero-copy view type is TokenDataZc:
| Field | Type | Description |
|---|
mint() | &Address | The mint this token account holds |
owner() | &Address | The wallet or program that owns this account |
amount() | u64 | Current token balance (in base units) |
delegate() | Option<&Address> | Approved delegate, if any |
state() | u8 | Raw account state (0 = uninitialized, 1 = initialized, 2 = frozen) |
delegated_amount() | u64 | Amount delegated to delegate |
close_authority() | Option<&Address> | Authority that can close this account, if any |
is_initialized() | bool | Whether state != 0 |
is_frozen() | bool | Whether state == 2 |
native_amount() | Option<u64> | SOL balance for native (wrapped SOL) accounts |
// Read token account fields — no copy, no allocation
let amount = self.vault_ta_a.amount();
let mint = self.vault_ta_a.mint();
let owner = self.vault_ta_a.owner();
The layout is fixed:
const _: () = assert!(core::mem::size_of::<TokenDataZc>() == 165);
Validating Accounts with #[account(...)]
The token, mint, and associated_token behavior arguments let you declaratively validate token accounts at instruction dispatch time. The framework calls the appropriate validation logic before your handler runs.
Validate a token account (mint + authority):
#[derive(Accounts)]
pub struct ValidateTokenCheck {
#[account(token(mint = mint, authority = authority, token_program = token_program))]
pub token_account: Account<Token>,
pub mint: Account<Mint>,
pub authority: Signer,
pub token_program: Program<TokenProgram>,
}
Validate a mint (authority + decimals + freeze authority):
#[derive(Accounts)]
pub struct ValidateMintCheck {
#[account(mint(authority = mint_authority, decimals = 6, freeze_authority = None, token_program = token_program))]
pub mint: Account<Mint>,
pub mint_authority: Signer,
pub token_program: Program<TokenProgram>,
}
Validate an associated token account:
#[derive(Accounts)]
pub struct ValidateAtaCheck {
#[account(associated_token(mint = mint, authority = wallet, token_program = token_program))]
pub ata: Account<Token>,
pub mint: Account<Mint>,
pub wallet: Signer,
pub token_program: Program<TokenProgram>,
}
Initializing Token Accounts
Add init (or init(idempotent)) alongside the behavior argument to have the framework run the necessary create_account + initialize_* CPIs before your handler executes.
Initialize a Mint
Provide decimals, authority, an optional freeze_authority, and the token_program. The framework calls system_program.create_account (with rent-exempt lamports) then token_program.initialize_mint2:#[derive(Accounts)]
pub struct InitMint {
#[account(mut)]
pub payer: Signer,
#[account(mut,
init,
mint(decimals = 6, authority = mint_authority, freeze_authority = None, token_program = token_program),
)]
pub mint: Account<Mint>,
pub mint_authority: Signer,
pub token_program: Program<TokenProgram>,
pub system_program: Program<SystemProgram>,
}
Initialize a Token Account
Provide mint, authority, and token_program. The framework allocates 165 bytes and calls initialize_account3:#[derive(Accounts)]
pub struct InitToken {
#[account(mut)]
pub payer: Signer,
#[account(mut,
init,
token(mint = mint, authority = payer, token_program = token_program),
)]
pub token_account: Account<Token>,
pub mint: Account<Mint>,
pub token_program: Program<TokenProgram>,
pub system_program: Program<SystemProgram>,
}
Initialize an Associated Token Account
Use associated_token(...) with ata_program and system_program. This routes through the ATA program’s Create instruction:#[derive(Accounts)]
pub struct InitAta {
#[account(mut)]
pub payer: Signer,
#[account(mut,
init,
associated_token(authority = wallet, mint = mint, token_program = token_program,
system_program = system_program, ata_program = ata_program),
)]
pub ata: Account<Token>,
pub wallet: Signer,
pub mint: Account<Mint>,
pub token_program: Program<TokenProgram>,
pub system_program: Program<SystemProgram>,
pub ata_program: Program<AssociatedTokenProgram>,
}
Idempotent Init (init-if-needed)
Use init(idempotent) to skip creation if the account already exists. This is safe to call repeatedly:#[account(mut,
init(idempotent),
token(mint = mint, authority = payer, token_program = token_program),
)]
pub token_account: Account<Token>,
TokenCpi Methods
TokenCpi is a trait implemented by Program<TokenProgram>. Import quasar_spl::prelude::* and call methods directly on your program field. Every method returns a CpiCall — chain .invoke() to execute or .invoke_signed(&seeds) to sign with a PDA.
transfer
Transfer tokens between two accounts. The authority must be the source account’s owner or an approved delegate.
#[derive(Accounts)]
pub struct Transfer {
pub authority: Signer,
#[account(mut)]
pub from: Account<Token>,
#[account(mut)]
pub to: Account<Token>,
pub token_program: Program<TokenProgram>,
}
impl Transfer {
pub fn handler(&self, amount: u64) -> Result<(), ProgramError> {
self.token_program
.transfer(&self.from, &self.to, &self.authority, amount)
.invoke()
}
}
Accounts: [WRITE] source, [WRITE] destination, [SIGNER] authority
transfer_checked
Transfer tokens while also verifying the mint’s decimal count. Prefer this over transfer when the decimal count is known.
#[derive(Accounts)]
pub struct TransferChecked {
pub authority: Signer,
#[account(mut)]
pub from: Account<Token>,
pub mint: Account<Mint>,
#[account(mut)]
pub to: Account<Token>,
pub token_program: Program<TokenProgram>,
}
impl TransferChecked {
pub fn handler(&self, amount: u64, decimals: u8) -> Result<(), ProgramError> {
self.token_program
.transfer_checked(
&self.from,
&self.mint,
&self.to,
&self.authority,
amount,
decimals,
)
.invoke()
}
}
Accounts: [WRITE] source, [] mint, [WRITE] destination, [SIGNER] authority
mint_to
Mint new tokens into a destination account. The caller must hold the mint authority.
#[derive(Accounts)]
pub struct MintTo {
pub authority: Signer,
#[account(mut)]
pub mint: Account<Mint>,
#[account(mut)]
pub to: Account<Token>,
pub token_program: Program<TokenProgram>,
}
impl MintTo {
pub fn handler(&self, amount: u64) -> Result<(), ProgramError> {
self.token_program
.mint_to(&self.mint, &self.to, &self.authority, amount)
.invoke()
}
}
Accounts: [WRITE] mint, [WRITE] destination, [SIGNER] mint authority
burn
Reduce the token supply by burning tokens from an account.
#[derive(Accounts)]
pub struct Burn {
pub authority: Signer,
#[account(mut)]
pub from: Account<Token>,
#[account(mut)]
pub mint: Account<Mint>,
pub token_program: Program<TokenProgram>,
}
impl Burn {
pub fn handler(&self, amount: u64) -> Result<(), ProgramError> {
self.token_program
.burn(&self.from, &self.mint, &self.authority, amount)
.invoke()
}
}
Accounts: [WRITE] source account, [WRITE] mint, [SIGNER] authority
close_account
Close a token account and send its remaining lamports to a destination. Use .invoke_signed(&seeds) when the authority is a PDA.
#[derive(Accounts)]
pub struct CloseTokenAccount {
#[account(mut)]
pub account: Account<Token>,
#[account(mut)]
pub destination: Signer,
#[account(dup)]
pub authority: Signer,
pub token_program: Program<TokenProgram>,
}
impl CloseTokenAccount {
pub fn handler(&self) -> Result<(), ProgramError> {
self.token_program
.close_account(&self.account, &self.destination, &self.authority)
.invoke()
}
}
Accounts: [WRITE] account to close, [WRITE] lamport destination, [SIGNER] close authority
approve
Grant a delegate permission to transfer up to amount tokens from the source.
#[derive(Accounts)]
pub struct Approve {
pub authority: Signer,
#[account(mut)]
pub source: Account<Token>,
pub delegate: UncheckedAccount,
pub token_program: Program<TokenProgram>,
}
impl Approve {
pub fn handler(&self, amount: u64) -> Result<(), ProgramError> {
self.token_program
.approve(&self.source, &self.delegate, &self.authority, amount)
.invoke()
}
}
Accounts: [WRITE] source, [] delegate, [SIGNER] owner
revoke
Revoke the current delegate’s authority.
#[derive(Accounts)]
pub struct Revoke {
pub authority: Signer,
#[account(mut)]
pub source: Account<Token>,
pub token_program: Program<TokenProgram>,
}
impl Revoke {
pub fn handler(&self) -> Result<(), ProgramError> {
self.token_program
.revoke(&self.source, &self.authority)
.invoke()
}
}
Accounts: [WRITE] source, [SIGNER] owner
PDA-Signed CPIs
When the authority is a program-derived address, pass the PDA seeds to .invoke_signed(). The seeds are stack-allocated Seed slices — no heap allocation required.
use quasar_lang::cpi::Seed;
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.taker_ta_a,
&self.escrow,
self.vault_ta_a.amount(),
)
.invoke_signed(&seeds)?;
Exit Behaviors: token_close and token_sweep
In addition to calling CPIs manually, you can attach close and sweep logic directly to an account field so the framework runs them automatically at instruction exit:
token_close — calls close_account after your handler returns:
#[account(
mut,
token_close(dest = receiver, authority = authority, token_program = token_program)
)]
pub vault: Account<Token>,
token_sweep — transfers all tokens out (via transfer_checked) before closing:
#[derive(Accounts)]
pub struct SweepAndClose {
pub authority: Signer,
#[account(
mut,
token(mint = mint, authority = authority, token_program = token_program),
token_sweep(receiver = receiver, mint = mint, authority = authority, token_program = token_program),
token_close(dest = destination, authority = authority, token_program = token_program)
)]
pub source: Account<Token>,
#[account(mut)]
pub receiver: Account<Token>,
pub mint: Account<Mint>,
#[account(mut)]
pub destination: UncheckedAccount,
pub token_program: Program<TokenProgram>,
}
token_sweep reads the balance and decimals from the zero-copy account data and builds a transfer_checked CPI. If the balance is already zero it is a no-op.
Full Example: Token Escrow
The escrow example in the repository uses quasar-spl throughout its three instructions. Here is how Make, Take, and Refund are implemented:
The maker deposits token A into a vault and creates a receive account for token B, both initialized idempotently:use {
crate::{ events::MakeEvent, state::{Escrow, EscrowInner} },
quasar_lang::prelude::*,
quasar_spl::prelude::*,
};
#[derive(Accounts)]
pub struct Make {
#[account(mut)]
pub maker: Signer,
#[account(init, payer = maker, address = Escrow::seeds(maker.address()))]
pub escrow: Account<Escrow>,
pub mint_a: Account<Mint>,
pub mint_b: Account<Mint>,
#[account(mut)]
pub maker_ta_a: Account<Token>,
#[account(init(idempotent), payer = maker, token(mint = mint_b, authority = maker, token_program = token_program))]
pub maker_ta_b: Account<Token>,
#[account(init(idempotent), payer = maker, token(mint = mint_a, authority = escrow, token_program = token_program))]
pub vault_ta_a: Account<Token>,
pub rent: Sysvar<Rent>,
pub token_program: Program<TokenProgram>,
pub system_program: Program<SystemProgram>,
}
impl Make {
pub fn deposit_tokens(&mut self, amount: u64) -> Result<(), ProgramError> {
self.token_program
.transfer(&self.maker_ta_a, &self.vault_ta_a, &self.maker, amount)
.invoke()
}
}
The taker sends token B to the maker and withdraws token A from the vault. Both vault transfer and close are PDA-signed:use {
crate::{events::TakeEvent, state::Escrow},
quasar_lang::{cpi::Seed, prelude::*},
quasar_spl::prelude::*,
};
#[derive(Accounts)]
pub struct Take {
#[account(mut)]
pub taker: Signer,
#[account(
mut,
has_one(maker),
has_one(maker_ta_b),
constraints(escrow.receive > 0),
close(dest = taker),
address = Escrow::seeds(maker.address())
)]
pub escrow: Account<Escrow>,
#[account(mut)]
pub maker: UncheckedAccount,
pub mint_a: Account<Mint>,
pub mint_b: Account<Mint>,
#[account(init(idempotent), payer = taker, token(mint = mint_a, authority = taker, token_program = token_program))]
pub taker_ta_a: Account<Token>,
#[account(mut)]
pub taker_ta_b: Account<Token>,
#[account(init(idempotent), payer = taker, token(mint = mint_b, authority = maker, token_program = token_program))]
pub maker_ta_b: Account<Token>,
#[account(mut)]
pub vault_ta_a: Account<Token>,
pub rent: Sysvar<Rent>,
pub token_program: Program<TokenProgram>,
pub system_program: Program<SystemProgram>,
}
impl Take {
pub fn transfer_tokens(&mut self) -> Result<(), ProgramError> {
self.token_program
.transfer(&self.taker_ta_b, &self.maker_ta_b, &self.taker, self.escrow.receive)
.invoke()
}
pub fn withdraw_tokens_and_close(&self, bumps: &TakeBumps) -> 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.taker_ta_a, &self.escrow, self.vault_ta_a.amount())
.invoke_signed(&seeds)?;
self.token_program
.close_account(&self.vault_ta_a, &self.taker, &self.escrow)
.invoke_signed(&seeds)
}
}
The maker cancels the escrow by transferring token A back and closing the vault. Both calls are PDA-signed:use {
crate::{events::RefundEvent, state::Escrow},
quasar_lang::{cpi::Seed, prelude::*},
quasar_spl::prelude::*,
};
#[derive(Accounts)]
pub struct Refund {
#[account(mut)]
pub maker: Signer,
#[account(
mut,
has_one(maker),
close(dest = maker),
address = Escrow::seeds(maker.address())
)]
pub escrow: Account<Escrow>,
pub mint_a: Account<Mint>,
#[account(init(idempotent), payer = maker, token(mint = mint_a, authority = maker, token_program = token_program))]
pub maker_ta_a: Account<Token>,
#[account(mut)]
pub vault_ta_a: Account<Token>,
pub rent: Sysvar<Rent>,
pub token_program: Program<TokenProgram>,
pub system_program: Program<SystemProgram>,
}
impl Refund {
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)
}
}
The escrow instructions use init(idempotent) on all token account fields so each instruction can be retried safely even if a previous attempt partially succeeded.