Crate pinocchio_tkn

Crate pinocchio_tkn 

Source
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 only
  • pinocchio-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 TokenAccount
  • helpers: Utility functions for calculations, validation, and detection
  • prelude: 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:

  1. Validate account ownership: Use helpers::assert_owned_by to ensure accounts are owned by the correct token program
  2. Check account state: Use helpers::assert_is_mint and helpers::assert_is_token_account to validate account structure
  3. Verify authorities: Use helpers::assert_mint_authority and similar validators
  4. Handle frozen accounts: Check helpers::assert_account_not_frozen before transfers
  5. Validate amounts: Use TransferChecked instead of Transfer to 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 of InitializeMint for cleaner API
  • Use InitializeAccount3 (preferred) for token accounts
  • Always use “Checked” variants (MintToChecked, TransferChecked) for safety
  • Program ID (TOKEN_PROGRAM_ID vs TOKEN_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:

  1. Calculate space with extensions
  2. Create account with correct size
  3. Initialize extensions FIRST
  4. 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 transfers
  • 02_transfer_fees.rs - Setting up and using transfer fees
  • 03_onchain_metadata.rs - Adding metadata to tokens
  • 04_full_workflow.rs - Complete token lifecycle
  • 05_validation_security.rs - Security best practices

§Program IDs

The crate provides constants for both token programs:

  • TOKEN_PROGRAM_ID: SPL Token (legacy) - TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
  • TOKEN_2022_PROGRAM_ID: Token-2022 - TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
  • ID: 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.
instructionsDeprecated
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