Expand description
§pinocchio-tkn
The complete Token toolkit for Pinocchio programs on Solana.
pinocchio-tkn provides zero-copy, zero-allocation CPI helpers for both SPL Token (legacy)
and Token-2022 programs, with full support for Token-2022 extensions.
§Features
- Complete Coverage: 60 instructions across SPL Token and Token-2022
- 19 common instructions (work with both programs)
- 41 Token-2022 exclusive extension instructions
- All 22 Token-2022 Extensions: Full implementation of 19 extensions, documented stubs for 3 confidential extensions
- Unified API: Single interface for both SPL Token and Token-2022
- Zero Allocations: Stack-only operations, perfect for Solana’s compute budget
- Type-Safe: Builder-style APIs with compile-time guarantees
- State Parsing: Zero-copy deserialization of Mint and TokenAccount data
- Comprehensive Helpers: Calculations, validations, and detection utilities
- Production Ready: Extensively tested with 100+ integration and E2E tests
§Why pinocchio-tkn?
While official packages are fragmented across multiple crates:
pinocchio-token: Legacy SPL Token onlypinocchio-token-2022: Core instructions only, no extensions- No unified interface between the two
pinocchio-tkn unifies everything into a single, complete package with a consistent API.
§Quick Start
Add to your Cargo.toml:
[dependencies]
pinocchio-tkn = "0.2"§Basic Transfer
// Works with both SPL Token and Token-2022
// Defaults to Token-2022 if program_id is None
Transfer {
source,
destination,
authority,
amount: 1_000_000,
program_id: None,
}.invoke()?;§Using Token-2022 Extensions
use pinocchio::account_info::AccountInfo;
use pinocchio_tkn::prelude::*;
use pinocchio_tkn::extensions::*;
// Initialize a mint with transfer fees
InitializeTransferFeeConfig {
mint,
transfer_fee_config_authority: Some(authority_key),
withdraw_withheld_authority: Some(authority_key),
transfer_fee_basis_points: 100, // 1%
maximum_fee: 5000,
}.invoke()?;
// Transfer with fees
TransferCheckedWithFee {
source,
mint,
destination,
authority,
amount: 1_000_000,
decimals: 9,
fee: 10_000,
}.invoke()?;§State Parsing
// Zero-copy parsing of account data
let mint = Mint::from_account_info(&mint_account)?;
let supply = mint.supply();
let decimals = mint.decimals();
let token_account = TokenAccount::from_account_info(&account)?;
if token_account.amount() < required_amount {
return Err(ProgramError::InsufficientFunds);
}§Module Organization
The crate is organized into feature-gated modules:
common: Instructions that work with both SPL Token and Token-2022 (19 instructions)extensions: Token-2022 exclusive extensions (41 instructions across 22 extensions)state: Zero-copy state parsing for Mint and TokenAccounthelpers: Utility functions for calculations, validation, and detectionprelude: Convenient re-exports of commonly used items
§Architecture Overview
┌─────────────────────────────────────────────────────────────┐
│ pinocchio-tkn │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────┐ ┌──────────────┐ ┌───────┐ ┌─────────┐ │
│ │ common │ │ extensions │ │ state │ │ helpers │ │
│ ├───────────┤ ├──────────────┤ ├───────┤ ├─────────┤ │
│ │Transfer │ │TransferFee │ │ Mint │ │ calc │ │
│ │MintTo │ │Metadata │ │Token │ │ valid │ │
│ │Burn │ │Interest │ │Account│ │ detect │ │
│ │Approve │ │Pointers │ └───────┘ └─────────┘ │
│ │... │ │Misc (13+) │ │
│ └───────────┘ └──────────────┘ │
│ │
├─────────────────────────────────────────────────────────────┤
│ Pinocchio (zero-alloc runtime) │
├─────────────────────────────────────────────────────────────┤
│ SPL Token │ Token-2022 │
│ (TokenkegQfe...VQ5DA) │ (TokenzQdBNb...xuEb) │
└─────────────────────────────────────────────────────────────┘§Feature Flags
The crate uses granular feature flags for optimal compile times and binary size:
# Default features (most commonly used)
default = ["common", "state", "helpers"]
# Core features
common = ["state"] # Common instructions (both programs)
state = [] # State parsing (Mint, TokenAccount)
helpers = [] # Calculation & validation utilities
# Extension groups (Token-2022 only)
ext-transfer-fee = [] # Transfer fee instructions (6)
ext-metadata = [] # On-chain metadata (5)
ext-interest = [] # Interest-bearing tokens (2)
ext-pointers = [] # Metadata/Group/Hook pointers (8)
ext-misc = [] # CPI Guard, Memo, Pause, etc (13)
# Convenience features
extensions = [ # All Token-2022 extensions
"ext-transfer-fee",
"ext-metadata",
"ext-interest",
"ext-pointers",
"ext-misc"
]
full = [ # Everything
"common",
"state",
"helpers",
"extensions"
]§Security Considerations
When building Solana programs with token operations, always:
- Validate account ownership: Use
helpers::assert_owned_byto ensure accounts are owned by the correct token program - Check account state: Use
helpers::assert_is_mintandhelpers::assert_is_token_accountto validate account structure - Verify authorities: Use
helpers::assert_mint_authorityand similar validators - Handle frozen accounts: Check
helpers::assert_account_not_frozenbefore transfers - Validate amounts: Use
TransferCheckedinstead ofTransferto verify decimals
§Token-2022 Extension Coverage
§Fully Implemented (19 extensions, 42 instructions)
- Transfer Fee (6 instructions): Assess fees on transfers
- Metadata Pointer (2): Point to on-chain or off-chain metadata
- Token Metadata (5): Store name, symbol, URI on-chain
- Transfer Hook (2): Custom program hooks on transfers
- Group Pointer (2): Token collection/grouping
- Group Member Pointer (2): Collection membership
- Default Account State (2): Auto-freeze new accounts
- Permanent Delegate (1): Immutable delegate authority
- Non-Transferable (1): Soul-bound tokens
- Interest Bearing (2): Accrue interest over time
- CPI Guard (2): Prevent CPI access to accounts
- Memo Transfer (2): Require memos on transfers
- Pausable (3): Pause/resume all token operations
- Scaled UI Amount (2): Non-standard decimal display
- Mint Close Authority (1): Allow mints to be closed
- Immutable Owner (1): Prevent ownership changes
- Reallocate (1): Resize accounts for new extensions
- Withdraw Excess Lamports (1): Clean up account rent
§Documented (3 extensions - ZK proof required)
These extensions require zero-knowledge cryptography and are provided as documented stubs with full explanations:
- Confidential Transfer: Private balance transfers using ElGamal encryption
- Confidential Transfer Fee: Fee collection with confidential amounts
- Confidential Mint/Burn: Private minting and burning operations
§Performance
All operations are designed for Solana’s constrained environment:
- Zero heap allocations: All data structures are stack-allocated
- Minimal compute units: Optimized instruction building
- Compile-time optimizations: Extensive use of
#[inline]and const functions - Small binary size: Granular feature flags reduce code size
§Usage Guides
§A. Basic Token Creation (SPL Token & Token-2022)
Creating a simple token without extensions works with both programs:
use pinocchio_tkn::prelude::*;
use pinocchio::system::CreateAccount;
// Step 1: Create and allocate the mint account
// For SPL Token or Token-2022 without extensions, mint size is 82 bytes
let mint_space = 82;
let rent_lamports = 0; /* calculate rent for 82 bytes */
// Create the mint account (using system program)
CreateAccount {
from: payer,
to: mint,
lamports: rent_lamports,
space: mint_space,
owner: &TOKEN_2022_PROGRAM_ID, // or &TOKEN_PROGRAM_ID for legacy
}.invoke()?;
// Step 2: Initialize the mint
InitializeMint2 {
mint,
mint_authority,
freeze_authority: Some(freeze_authority),
decimals: 9, // 9 decimals like SOL
}.invoke()?;
// Step 3: Create a token account for a user
let account_space = 165;
let account_rent = 0; /* calculate rent for 165 bytes */
CreateAccount {
from: payer,
to: user_token_account,
lamports: account_rent,
space: account_space,
owner: &TOKEN_2022_PROGRAM_ID,
}.invoke()?;
InitializeAccount3 {
account: user_token_account,
mint,
owner: user_owner,
}.invoke()?;
// Step 4: Mint tokens to the user
MintToChecked {
mint,
account: user_token_account,
mint_authority: authority,
amount: 1_000_000_000, // 1 token with 9 decimals
decimals: 9,
}.invoke()?;
// Step 5: Transfer tokens between accounts
TransferChecked {
source: user_token_account,
mint,
destination: recipient_token_account,
authority: user,
amount: 100_000_000, // 0.1 token
decimals: 9,
}.invoke()?;Key Points:
- Use
InitializeMint2(preferred) instead ofInitializeMintfor cleaner API - Use
InitializeAccount3(preferred) for token accounts - Always use “Checked” variants (
MintToChecked,TransferChecked) for safety - Program ID (
TOKEN_PROGRAM_IDvsTOKEN_2022_PROGRAM_ID) must match account owner
§B. Token-2022 with Single Extension
Adding transfer fees to a token:
use pinocchio_tkn::prelude::*;
use pinocchio_tkn::helpers::{mint_space_for_extensions, ExtensionType, account_space_for_extensions};
use pinocchio::system::CreateAccount;
// Step 1: Calculate space needed for mint with TransferFeeConfig extension
let extensions = &[ExtensionType::TransferFeeConfig];
let mint_space = mint_space_for_extensions(extensions);
// Result: 82 (base) + 3 (header) + 108 (fee config) = 193 bytes
let rent_lamports = 0; /* calculate rent for mint_space */
// Step 2: Create the mint account with correct size
CreateAccount {
from: payer,
to: mint,
lamports: rent_lamports,
space: mint_space,
owner: &TOKEN_2022_PROGRAM_ID, // Must use Token-2022 for extensions!
}.invoke()?;
// Step 3: Initialize the TransferFee extension BEFORE initializing the mint
InitializeTransferFeeConfig {
mint,
transfer_fee_config_authority: Some(authority_key),
withdraw_withheld_authority: Some(authority_key),
transfer_fee_basis_points: 100, // 1% fee (100 basis points)
maximum_fee: 5_000_000, // 0.005 token max fee (with 9 decimals)
}.invoke()?;
// Step 4: NOW initialize the mint (after extensions)
InitializeMint2 {
mint,
mint_authority,
freeze_authority: Some(freeze_authority),
decimals: 9,
}.invoke()?;
// Step 5: Create token accounts (inherit TransferFeeAmount extension)
let account_extensions = &[ExtensionType::TransferFeeConfig];
let account_space = account_space_for_extensions(account_extensions);
// Accounts automatically get TransferFeeAmount extension
let account_rent = 0;
CreateAccount {
from: payer,
to: user_account,
lamports: account_rent,
space: account_space,
owner: &TOKEN_2022_PROGRAM_ID,
}.invoke()?;
InitializeAccount3 {
account: user_account,
mint,
owner: user_owner,
}.invoke()?;
// Step 6: Transfer with fees
use pinocchio_tkn::helpers::calculate_transfer_fee;
let amount = 100_000_000; // 0.1 token
let fee = calculate_transfer_fee(amount, 100, 5_000_000); // Calculate fee first
TransferCheckedWithFee {
source: user_account,
mint,
destination: recipient_account,
authority: user,
amount,
decimals: 9,
fee, // Must provide calculated fee
}.invoke()?;
// Step 7: Withdraw collected fees (authority only)
WithdrawWithheldTokensFromAccounts {
mint,
destination: fee_vault,
withdraw_withheld_authority: authority,
sources: &[user_account, recipient_account], // Accounts to withdraw from
}.invoke()?;Critical Order:
- Calculate space with extensions
- Create account with correct size
- Initialize extensions FIRST
- Initialize mint LAST
§C. Token-2022 with Multiple Extensions (Advanced)
Creating a premium token with Transfer Fees + Metadata + Mint Close Authority:
use pinocchio_tkn::prelude::*;
use pinocchio_tkn::helpers::{mint_space_for_extensions, ExtensionType, account_space_for_extensions};
use pinocchio::system::CreateAccount;
// Step 1: Calculate space for ALL extensions
let extensions = &[
ExtensionType::TransferFeeConfig, // 108 bytes
ExtensionType::MetadataPointer, // 32 bytes
ExtensionType::MintCloseAuthority, // 32 bytes
];
let mint_space = mint_space_for_extensions(extensions);
// = 82 + 3 + 108 + 32 + 32 = 257 bytes
let rent_lamports = 0; /* calculate rent */
// Step 2: Create mint account
CreateAccount {
from: payer,
to: mint,
lamports: rent_lamports,
space: mint_space,
owner: &TOKEN_2022_PROGRAM_ID,
}.invoke()?;
// Step 3a: Initialize TransferFee extension
InitializeTransferFeeConfig {
mint,
transfer_fee_config_authority: Some(authority_key),
withdraw_withheld_authority: Some(authority_key),
transfer_fee_basis_points: 50, // 0.5%
maximum_fee: 10_000_000,
}.invoke()?;
// Step 3b: Initialize MetadataPointer extension
InitializeMetadataPointer {
mint,
authority: Some(authority_key),
metadata_address: Some(mint_key), // Store metadata on the mint itself
}.invoke()?;
// Step 3c: Initialize MintCloseAuthority extension
InitializeMintCloseAuthority {
mint,
close_authority: Some(authority_key),
}.invoke()?;
// Step 4: Initialize the mint (AFTER all extensions)
InitializeMint2 {
mint,
mint_authority,
freeze_authority: Some(freeze_authority),
decimals: 6, // USDC-style decimals
}.invoke()?;
// Step 5: Initialize on-mint metadata (AFTER mint initialization)
InitializeTokenMetadata {
metadata: mint, // Using the mint itself as metadata storage
update_authority: authority,
mint,
mint_authority: authority,
name: "Premium Token",
symbol: "PREM",
uri: "https://example.com/premium-token.json",
}.invoke()?;
// Step 6: Create token accounts (with inherited extensions)
let account_space = account_space_for_extensions(extensions);
let account_rent = 0;
CreateAccount {
from: payer,
to: user_account,
lamports: account_rent,
space: account_space,
owner: &TOKEN_2022_PROGRAM_ID,
}.invoke()?;
InitializeAccount3 {
account: user_account,
mint,
owner: user_owner,
}.invoke()?;
// Step 7: Use the token normally
MintToChecked {
mint,
account: user_account,
mint_authority: authority,
amount: 1_000_000, // 1 PREM
decimals: 6,
}.invoke()?;
// Step 8: Later, close the mint when done (requires MintCloseAuthority)
// First, burn all supply...
// Then close the mint:
CloseAccount {
account: mint,
destination: authority, // Rent refund destination
authority,
}.invoke()?;Extension Initialization Order Rules:
┌─────────────────────────────────────────────────────────┐
│ BEFORE InitializeMint2: │
│ - TransferFeeConfig │
│ - MetadataPointer │
│ - GroupPointer │
│ - GroupMemberPointer │
│ - TransferHook │
│ - InterestBearingConfig │
│ - PermanentDelegate │
│ - NonTransferableMint │
│ - MintCloseAuthority │
│ - DefaultAccountState │
│ - Pausable │
│ - ScaledUiAmount │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ InitializeMint2 or InitializeMint │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ AFTER InitializeMint2: │
│ - InitializeTokenMetadata (if using MetadataPointer) │
└─────────────────────────────────────────────────────────┘§D. Common Patterns
§Detecting Which Program a Token Uses
use pinocchio_tkn::helpers::{is_token_2022, is_spl_token, has_extensions};
if is_token_2022(&mint_account) {
// This is a Token-2022 mint
if has_extensions(&mint_account, true) {
// Has extensions - might have fees, metadata, etc.
// Be careful with transfers!
} else {
// Token-2022 but no extensions - same as legacy
}
} else if is_spl_token(&mint_account) {
// Legacy SPL Token - no extensions possible
}§Reading Token State and Extensions
use pinocchio_tkn::state::{Mint, TokenAccount};
use pinocchio_tkn::helpers::has_extensions;
// Read mint state
let mint = Mint::from_account_info(&mint_account)?;
let supply = mint.supply();
let decimals = mint.decimals();
if let Some(authority) = mint.mint_authority() {
// Minting is enabled
} else {
// Supply is fixed - no more minting possible
}
// Read token account state
let token = TokenAccount::from_account_info(&account)?;
let balance = token.amount();
let owner = token.owner();
if token.is_frozen() {
return Err(ProgramError::InvalidAccountData); // Cannot transfer
}
// Access extension data (Token-2022 only)
if has_extensions(&mint_account, true) {
let raw_data = mint.raw_data();
// Extension data starts at byte 82
// Parse extension header and data manually if needed
}§Security Validation Pattern
Always validate accounts before performing operations:
use pinocchio_tkn::helpers::*;
use pinocchio_tkn::state::Mint;
use pinocchio_tkn::TransferChecked;
fn secure_transfer(
source: &AccountInfo,
destination: &AccountInfo,
mint: &AccountInfo,
authority: &AccountInfo,
amount: u64,
) -> Result<(), ProgramError> {
// 1. Validate ownership
let program_id = get_token_program_id(mint);
assert_owned_by(source, program_id)?;
assert_owned_by(destination, program_id)?;
assert_owned_by(mint, program_id)?;
// 2. Validate account types
assert_is_mint(mint)?;
assert_is_token_account(source, Some(mint.key()), None)?;
assert_is_token_account(destination, Some(mint.key()), None)?;
// 3. Validate state
assert_account_not_frozen(source)?;
assert_account_not_frozen(destination)?;
// 4. Validate authority
assert_token_account_owner(source, authority)?;
// 5. All checks passed - safe to transfer
TransferChecked {
source,
mint,
destination,
authority,
amount,
decimals: Mint::from_account_info(mint)?.decimals(),
program_id: None,
}.invoke()?;
Ok(())
}§Fee Calculations
use pinocchio_tkn::helpers::{calculate_transfer_fee, calculate_inverse_transfer_fee};
// Scenario 1: User wants to send 100 tokens, how much fee?
let amount = 100_000_000; // 100 tokens (6 decimals)
let fee_bps = 100; // 1%
let max_fee = 5_000_000; // 5 tokens max
let fee = calculate_transfer_fee(amount, fee_bps, max_fee);
// fee = 1_000_000 (1 token)
// Recipient receives: 100_000_000 - 1_000_000 = 99_000_000
// Scenario 2: User wants recipient to receive EXACTLY 100 tokens, how much to send?
let desired_amount = 100_000_000;
let (amount_to_send, fee) = calculate_inverse_transfer_fee(
desired_amount,
fee_bps,
max_fee,
);
// amount_to_send = 101_010_101
// fee = 1_010_101
// Recipient receives: 101_010_101 - 1_010_101 = 100_000_000 ✓§Examples
Check the examples/ directory for complete working examples:
01_basic_transfer.rs- Simple SPL Token and Token-2022 transfers02_transfer_fees.rs- Setting up and using transfer fees03_onchain_metadata.rs- Adding metadata to tokens04_full_workflow.rs- Complete token lifecycle05_validation_security.rs- Security best practices
§Program IDs
The crate provides constants for both token programs:
TOKEN_PROGRAM_ID: SPL Token (legacy) -TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DATOKEN_2022_PROGRAM_ID: Token-2022 -TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEbID: Alias for Token-2022 (default)
§Compatibility
- Rust Version: 1.79 or later
- no_std: Fully compatible with no_std environments
- Pinocchio: Built on pinocchio 0.9+ for zero-allocation runtime
- Solana: Compatible with Solana SDK 3.0+
Modules§
- common
- Common instructions (work with both SPL Token and Token-2022) Common token instructions that work with both SPL Token and Token-2022.
- extensions
- Token-2022 exclusive extensions Token-2022 exclusive extensions.
- helpers
- Utility functions (calculations, validation, detection) Utility functions for Token operations.
- instructions
Deprecated - Legacy module re-exports (backward compatibility)
- macros
- Macros for building variable-length account lists in no_std environments.
- prelude
- Convenient prelude with all common types
- state
- Account state parsing (Mint, TokenAccount) Zero-copy state parsing for Token accounts and Mints.
Macros§
- invoke_
with_ var_ accounts - Invokes an instruction with a variable number of account infos.
Constants§
- ID
- Alias for Token-2022 (default, backward compatibility)
- TOKEN_
2022_ PROGRAM_ ID - Token-2022 Program ID:
TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb - TOKEN_
PROGRAM_ ID - SPL Token (legacy) Program ID:
TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA