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.

Token-2022 (also called Token Extensions) is Solana’s successor to the original SPL Token program. It lives at a different program address and adds optional extension data after the standard mint and token account layouts. quasar-spl supports Token-2022 with dedicated account wrappers (Token2022, Mint2022), a shared zero-copy data layout for the base fields, and a unified Interface<TokenInterface> wrapper that accepts either program at runtime.

Program Addresses

Both token program addresses are exported from quasar-spl:
use quasar_spl::{SPL_TOKEN_ID, TOKEN_2022_ID, ATA_PROGRAM_ID};
ConstantAddress
SPL_TOKEN_IDTokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
TOKEN_2022_IDTokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
ATA_PROGRAM_IDATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL
Token2022Program is the program marker type for Token-2022. Its Id::ID constant is TOKEN_2022_ID:
// Require Token-2022 — rejects any other program address
pub token_program: Program<Token2022Program>,

Account Types for Token-2022

quasar-spl exports two Token-2022-specific wrappers that enforce ownership by TOKEN_2022_ID:
WrapperOwner enforcedUnderlying layout
Account<Token2022>TOKEN_2022_ID onlyTokenDataZc (165 bytes)
Account<Mint2022>TOKEN_2022_ID onlyMintDataZc (82 bytes)
Both wrappers share the same zero-copy data layouts (TokenDataZc and MintDataZc) as their SPL Token counterparts. The base fields — mint, owner, amount, supply, decimals, etc. — are identical at the byte level. Token-2022 extension data, when present, appears after byte 165 (for token accounts) or byte 82 (for mints) and is not parsed by these types.
Token-2022 extension data lives beyond the base 165-byte token / 82-byte mint layout. Quasar-spl gives you zero-copy access to the base fields only. If your program needs to read extension-specific data (e.g. transfer fees, interest-bearing configuration), you must parse the extension TLV region manually.

Using Token-2022 Account Types

Swap TokenToken2022 and MintMint2022, and change the program marker to Token2022Program. All CPI methods remain identical. MintTo with Token-2022:
use {quasar_derive::Accounts, quasar_lang::prelude::*, quasar_spl::prelude::*};

#[derive(Accounts)]
pub struct MintToT22 {
    pub authority: Signer,
    #[account(mut)]
    pub mint: Account<Mint2022>,
    #[account(mut)]
    pub to: Account<Token2022>,
    pub token_program: Program<Token2022Program>,
}

impl MintToT22 {
    pub fn handler(&self, amount: u64) -> Result<(), ProgramError> {
        self.token_program
            .mint_to(&self.mint, &self.to, &self.authority, amount)
            .invoke()
    }
}
Burn with Token-2022:
use {quasar_derive::Accounts, quasar_lang::prelude::*, quasar_spl::prelude::*};

#[derive(Accounts)]
pub struct BurnT22 {
    pub authority: Signer,
    #[account(mut)]
    pub from: Account<Token2022>,
    #[account(mut)]
    pub mint: Account<Mint2022>,
    pub token_program: Program<Token2022Program>,
}

impl BurnT22 {
    pub fn handler(&self, amount: u64) -> Result<(), ProgramError> {
        self.token_program
            .burn(&self.from, &self.mint, &self.authority, amount)
            .invoke()
    }
}

Initializing Token-2022 Accounts

The #[account(init, ...)] attribute works identically for Token-2022 types. Swap the type and program marker — everything else is unchanged. Initialize a Token-2022 mint:
#[derive(Accounts)]
pub struct InitMintT22 {
    #[account(mut)]
    pub payer: Signer,
    #[account(mut,
        init,
        mint(decimals = 6, authority = mint_authority, freeze_authority = None, token_program = token_program),
    )]
    pub mint: Account<Mint2022>,
    pub mint_authority: Signer,
    pub token_program: Program<Token2022Program>,
    pub system_program: Program<SystemProgram>,
}
Initialize a Token-2022 token account:
#[derive(Accounts)]
pub struct InitTokenT22 {
    #[account(mut)]
    pub payer: Signer,
    #[account(mut,
        init,
        token(mint = mint, authority = payer, token_program = token_program),
    )]
    pub token_account: Account<Token2022>,
    pub mint: Account<Mint2022>,
    pub token_program: Program<Token2022Program>,
    pub system_program: Program<SystemProgram>,
}

Interface<TokenInterface> — Accepting Either Program

Interface<TokenInterface> is a runtime-polymorphic program marker that accepts both SPL_TOKEN_ID and TOKEN_2022_ID. The TokenInterface struct in spl/src/interface.rs implements ProgramInterface and its matches function approves both addresses. Combined with InterfaceAccount<Token> and InterfaceAccount<Mint>, you can write a single instruction that works with tokens from either program.
use {quasar_derive::Accounts, quasar_lang::prelude::*, quasar_spl::prelude::*};

#[derive(Accounts)]
pub struct InterfaceTransfer {
    pub authority: Signer,
    #[account(mut)]
    pub from: InterfaceAccount<Token>,
    #[account(mut)]
    pub to: InterfaceAccount<Token>,
    pub token_program: Interface<TokenInterface>,
}

impl InterfaceTransfer {
    pub fn handler(&self, amount: u64) -> Result<(), ProgramError> {
        self.token_program
            .transfer(&self.from, &self.to, &self.authority, amount)
            .invoke()
    }
}
At instruction dispatch the framework validates that:
  • The token_program account’s address is either SPL_TOKEN_ID or TOKEN_2022_ID.
  • Each InterfaceAccount<Token> is owned by that same program.
The CPI call on the Interface<TokenInterface> field dispatches to whichever program address is stored in the account at runtime — there is no branching in user code. transfer_checked via interface:
#[derive(Accounts)]
pub struct TransferCheckedInterface {
    pub authority: Signer,
    #[account(mut)]
    pub from: InterfaceAccount<Token>,
    pub mint: InterfaceAccount<Mint>,
    #[account(mut)]
    pub to: InterfaceAccount<Token>,
    pub token_program: Interface<TokenInterface>,
}

impl TransferCheckedInterface {
    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()
    }
}

Validating Interface Accounts

The token and mint behavior arguments work with InterfaceAccount<T> the same way they work with Account<T>. When token_program = token_program is provided the validator checks that the account is owned by that specific program and that the program is one of the two accepted addresses. Validate a token account via interface:
#[derive(Accounts)]
pub struct ValidateTokenInterfaceCheck {
    #[account(token(mint = mint, authority = authority, token_program = token_program))]
    pub token_account: InterfaceAccount<Token>,
    pub mint: InterfaceAccount<Mint>,
    pub authority: Signer,
    pub token_program: Interface<TokenInterface>,
}
Validate a mint via interface:
#[derive(Accounts)]
pub struct ValidateMintInterfaceCheck {
    #[account(mint(authority = mint_authority, decimals = 6, freeze_authority = None, token_program = token_program))]
    pub mint: InterfaceAccount<Mint>,
    pub mint_authority: Signer,
    pub token_program: Interface<TokenInterface>,
}

Initializing with Interface<TokenInterface>

The init system also works through Interface<TokenInterface>. At init time the framework reads the program address from the supplied token_program account and dispatches to the correct initialize_account3 or initialize_mint2 instruction:
#[derive(Accounts)]
pub struct InitTokenInterface {
    #[account(mut)]
    pub payer: Signer,
    #[account(mut,
        init,
        token(mint = mint, authority = payer, token_program = token_program),
    )]
    pub token_account: InterfaceAccount<Token>,
    pub mint: InterfaceAccount<Mint>,
    pub token_program: Interface<TokenInterface>,
    pub system_program: Program<SystemProgram>,
}
#[derive(Accounts)]
pub struct InitMintInterface {
    #[account(mut)]
    pub payer: Signer,
    #[account(mut,
        init,
        mint(decimals = 6, authority = mint_authority, freeze_authority = None, token_program = token_program),
    )]
    pub mint: InterfaceAccount<Mint>,
    pub mint_authority: Signer,
    pub token_program: Interface<TokenInterface>,
    pub system_program: Program<SystemProgram>,
}

Choosing the Right Type

Use Account<Token> / Account<Mint> with Program<TokenProgram>. The owner is enforced at compile time as SPL_TOKEN_ID. This is the lowest-overhead option when you know the program at design time.
pub vault: Account<Token>,
pub mint:  Account<Mint>,
pub token_program: Program<TokenProgram>,

Migration: SPL Token → Token-2022

If you are upgrading an existing program to support Token-2022 while keeping backward compatibility with SPL Token, the migration path is:
1

Change account types to InterfaceAccount

Replace Account<Token> with InterfaceAccount<Token> and Account<Mint> with InterfaceAccount<Mint> in your accounts struct. Both still dereference to the same TokenDataZc / MintDataZc zero-copy view.
2

Change the program marker to Interface<TokenInterface>

Replace Program<TokenProgram> with Interface<TokenInterface>. All TokenCpi method calls remain identical — no changes to your handler logic.
3

Pass token_program to behavior args

If you use #[account(token(...))] or #[account(mint(...))] annotations, add token_program = token_program to those args so the validator checks both owner and program address:
#[account(token(mint = mint, authority = authority, token_program = token_program))]
pub token_account: InterfaceAccount<Token>,
4

Handle Token-2022 extension data separately if needed

If your program reads extension-specific fields (e.g. TransferFeeConfig, InterestBearingConfig), you must parse the TLV extension region beyond byte 165 / 82 manually. Quasar-spl’s zero-copy wrappers cover only the base layout.
Token-2022 mint and token accounts may be larger than the base 82 / 165 bytes when extensions are enabled. Do not assume a fixed account size when pre-computing rent-exempt thresholds for Token-2022 accounts that carry extensions.
If you need to support both legacy SPL Token wallets and new Token-2022 wallets in the same instruction, Interface<TokenInterface> is the right choice. The validator ensures both the account owner and the program account itself are the same token program, preventing cross-program token account confusion.

Build docs developers (and LLMs) love