Skip to main content

Overview

Sava provides comprehensive support for SPL Token and Token-2022 programs, including account parsing, filters, and extension handling. This guide covers token account operations, mint management, and Token-2022 features.

Working with Token Accounts

Token Account Structure

Token accounts store user token balances:
import software.sava.core.accounts.token.TokenAccount;
import software.sava.core.accounts.PublicKey;

public record TokenAccount(
    PublicKey address,
    PublicKey mint,              // Token mint address
    PublicKey owner,             // Account owner
    long amount,                 // Token balance
    int delegateOption,          // 0 or 1
    PublicKey delegate,          // Optional delegate
    AccountState state,          // Initialized, Frozen, or Uninitialized
    int isNativeOption,          // 0 or 1
    long isNative,               // Native SOL amount if wrapped
    long delegatedAmount,        // Amount delegated
    int closeAuthorityOption,    // 0 or 1
    PublicKey closeAuthority     // Optional close authority
) {}
Source: TokenAccount.java:13-24

Reading Token Accounts

import software.sava.core.accounts.token.TokenAccount;
import software.sava.rpc.json.http.client.SolanaRpcClient;
import software.sava.rpc.json.http.response.AccountInfo;

PublicKey tokenAccountAddress = PublicKey.fromBase58Encoded("TokenAccountAddress");

// Fetch and parse token account
CompletableFuture<AccountInfo<TokenAccount>> accountFuture = 
    rpcClient.getAccountInfo(
        tokenAccountAddress,
        TokenAccount.FACTORY
    );

AccountInfo<TokenAccount> accountInfo = accountFuture.join();
TokenAccount tokenAccount = accountInfo.data();

System.out.println("Mint: " + tokenAccount.mint().toBase58());
System.out.println("Owner: " + tokenAccount.owner().toBase58());
System.out.println("Balance: " + tokenAccount.amount());
System.out.println("State: " + tokenAccount.state());
Source: TokenAccount.java:57-61

Account State

import software.sava.core.accounts.token.AccountState;

TokenAccount account = // ... fetch token account

switch (account.state()) {
    case Uninitialized:
        System.out.println("Account not yet initialized");
        break;
    case Initialized:
        System.out.println("Account is active");
        break;
    case Frozen:
        System.out.println("Account is frozen");
        break;
}

Filtering Token Accounts

Filter by Owner

import software.sava.core.rpc.Filter;
import software.sava.core.accounts.token.TokenAccount;
import java.util.List;

PublicKey owner = PublicKey.fromBase58Encoded("OwnerAddress");
PublicKey tokenProgramId = PublicKey.fromBase58Encoded(
    "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
);

// Create filters
List<Filter> filters = List.of(
    TokenAccount.TOKEN_ACCOUNT_SIZE_FILTER,  // 165 bytes
    TokenAccount.createOwnerFilter(owner)     // Owner at offset 32
);

// Get all token accounts for owner
CompletableFuture<List<AccountInfo<TokenAccount>>> accountsFuture =
    rpcClient.getProgramAccounts(
        tokenProgramId,
        filters,
        TokenAccount.FACTORY
    );

List<AccountInfo<TokenAccount>> accounts = accountsFuture.join();
for (AccountInfo<TokenAccount> info : accounts) {
    TokenAccount account = info.data();
    System.out.println("Account: " + account.address().toBase58());
    System.out.println("Mint: " + account.mint().toBase58());
    System.out.println("Balance: " + account.amount());
}
Source: TokenAccount.java:27, TokenAccount.java:45-46

Filter by Mint

import software.sava.core.rpc.Filter;

PublicKey mint = PublicKey.fromBase58Encoded("MintAddress");

// Filter for specific token mint
List<Filter> filters = List.of(
    TokenAccount.TOKEN_ACCOUNT_SIZE_FILTER,
    TokenAccount.createMintFilter(mint)
);

// Get all accounts holding this token
var accountsFuture = rpcClient.getProgramAccounts(
    tokenProgramId,
    filters,
    TokenAccount.FACTORY
);

List<AccountInfo<TokenAccount>> accounts = accountsFuture.join();
System.out.println("Found " + accounts.size() + " token accounts");
Source: TokenAccount.java:41-43

Filter by Delegate

PublicKey delegate = PublicKey.fromBase58Encoded("DelegateAddress");

List<Filter> filters = List.of(
    TokenAccount.TOKEN_ACCOUNT_SIZE_FILTER,
    TokenAccount.createDelegateFilter(delegate)
);

var accountsFuture = rpcClient.getProgramAccounts(
    tokenProgramId,
    filters,
    TokenAccount.FACTORY
);
Source: TokenAccount.java:49-51
1
Step 1: Define Filters
2
Create filters for account size and specific field values.
3
Step 2: Query Program Accounts
4
Use getProgramAccounts() with the Token Program ID and filters.
5
Step 3: Parse Results
6
Use TokenAccount.FACTORY to deserialize account data.
7
Step 4: Process Accounts
8
Iterate through results and extract token information.

RPC Methods for Token Accounts

Get Token Accounts by Owner

import software.sava.rpc.json.http.client.SolanaRpcClient;
import java.util.concurrent.CompletableFuture;

PublicKey owner = PublicKey.fromBase58Encoded("OwnerAddress");
PublicKey mint = PublicKey.fromBase58Encoded("MintAddress");

// Get token accounts for specific mint
CompletableFuture<List<AccountInfo<TokenAccount>>> accountsFuture =
    rpcClient.getTokenAccountsForTokenMintByOwner(owner, mint);

List<AccountInfo<TokenAccount>> accounts = accountsFuture.join();
Source: SolanaRpcClient.java:796-801

Get Token Accounts by Program

import software.sava.core.accounts.PublicKey;

PublicKey owner = PublicKey.fromBase58Encoded("OwnerAddress");
PublicKey tokenProgramId = PublicKey.fromBase58Encoded(
    "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
);

// Get all token accounts for owner (any mint)
CompletableFuture<List<AccountInfo<TokenAccount>>> accountsFuture =
    rpcClient.getTokenAccountsForProgramByOwner(owner, tokenProgramId);
Source: SolanaRpcClient.java:803-808

Get Token Balance

import software.sava.rpc.json.http.response.TokenAmount;

PublicKey tokenAccount = PublicKey.fromBase58Encoded("TokenAccountAddress");

CompletableFuture<TokenAmount> balanceFuture =
    rpcClient.getTokenAccountBalance(tokenAccount);

TokenAmount balance = balanceFuture.join();
System.out.println("Amount: " + balance.amount());
System.out.println("Decimals: " + balance.decimals());
System.out.println("UI Amount: " + balance.uiAmountString());
Source: SolanaRpcClient.java:778-780

Token-2022 (Token Extensions)

Token-2022 is the new token program with extension support.

Reading Token-2022 Accounts

import software.sava.core.accounts.token.Token2022;
import software.sava.core.accounts.token.Mint;
import software.sava.core.accounts.token.extensions.ExtensionType;
import software.sava.core.accounts.token.extensions.TokenExtension;
import java.util.Map;

PublicKey token2022Mint = PublicKey.fromBase58Encoded("Token2022MintAddress");

// Fetch Token-2022 account
CompletableFuture<AccountInfo<Token2022>> accountFuture =
    rpcClient.getAccountInfo(
        token2022Mint,
        Token2022.FACTORY
    );

AccountInfo<Token2022> accountInfo = accountFuture.join();
Token2022 token = accountInfo.data();

// Access base mint data
Mint mint = token.mint();
System.out.println("Supply: " + mint.supply());
System.out.println("Decimals: " + mint.decimals());

// Access extensions
Map<ExtensionType, TokenExtension> extensions = token.extensions();
for (ExtensionType type : extensions.keySet()) {
    System.out.println("Extension: " + type);
}
Source: Token2022.java:12-14, Token2022.java:18

Token-2022 Extensions

Token-2022 supports various extensions:
import software.sava.core.accounts.token.extensions.*;

Token2022 token = // ... fetch token
Map<ExtensionType, TokenExtension> extensions = token.extensions();

// Check for specific extension
if (extensions.containsKey(ExtensionType.TransferFeeConfig)) {
    TransferFeeConfig feeConfig = (TransferFeeConfig) 
        extensions.get(ExtensionType.TransferFeeConfig);
    System.out.println("Transfer fee configured");
}

if (extensions.containsKey(ExtensionType.MetadataPointer)) {
    MetadataPointer metadata = (MetadataPointer)
        extensions.get(ExtensionType.MetadataPointer);
    System.out.println("Has metadata pointer");
}

if (extensions.containsKey(ExtensionType.TransferHook)) {
    TransferHook hook = (TransferHook)
        extensions.get(ExtensionType.TransferHook);
    System.out.println("Has transfer hook");
}

Available Extensions

Token-2022 supports these extensions:
  • TransferFeeConfig - Transfer fees
  • TransferFeeAmount - Fee amounts
  • MintCloseAuthority - Mint close authority
  • ConfidentialTransferMint - Confidential transfers
  • DefaultAccountState - Default account state
  • ImmutableOwner - Immutable ownership
  • MemoTransfer - Required memo on transfer
  • NonTransferable - Non-transferable tokens
  • InterestBearingConfig - Interest-bearing tokens
  • PermanentDelegate - Permanent delegate
  • TransferHook - Transfer hook program
  • MetadataPointer - Token metadata pointer
  • TokenMetadata - Embedded metadata
  • GroupPointer - Token group pointer
  • GroupMemberPointer - Group member pointer
Source: Token2022.java:22-58

Mint Operations

Reading Mint Account

import software.sava.core.accounts.token.Mint;

PublicKey mintAddress = PublicKey.fromBase58Encoded("MintAddress");

// For Token-2022, Mint is part of Token2022 record
Token2022 token = // ... fetch from RPC
Mint mint = token.mint();

System.out.println("Mint Authority: " + 
    (mint.mintAuthority() != null ? mint.mintAuthority().toBase58() : "None"));
System.out.println("Supply: " + mint.supply());
System.out.println("Decimals: " + mint.decimals());
System.out.println("Freeze Authority: " +
    (mint.freezeAuthority() != null ? mint.freezeAuthority().toBase58() : "None"));

Get Token Supply

import software.sava.rpc.json.http.response.TokenAmount;

PublicKey mintAddress = PublicKey.fromBase58Encoded("MintAddress");

CompletableFuture<TokenAmount> supplyFuture =
    rpcClient.getTokenSupply(mintAddress);

TokenAmount supply = supplyFuture.join();
System.out.println("Total Supply: " + supply.amount());
System.out.println("UI Amount: " + supply.uiAmountString());
Source: SolanaRpcClient.java:815-817

Get Largest Token Accounts

import software.sava.rpc.json.http.response.AccountTokenAmount;

PublicKey mintAddress = PublicKey.fromBase58Encoded("MintAddress");

CompletableFuture<List<AccountTokenAmount>> largestFuture =
    rpcClient.getTokenLargestAccounts(mintAddress);

List<AccountTokenAmount> largest = largestFuture.join();
for (AccountTokenAmount account : largest) {
    System.out.println("Address: " + account.address().toBase58());
    System.out.println("Amount: " + account.amount().amount());
}
Source: SolanaRpcClient.java:810-813

WebSocket Subscriptions

Subscribe to token account changes in real-time:
import software.sava.rpc.json.http.ws.SolanaRpcWebsocket;
import software.sava.rpc.json.http.response.AccountInfo;
import java.net.http.HttpClient;

// Create WebSocket client
HttpClient httpClient = HttpClient.newHttpClient();
SolanaRpcWebsocket ws = SolanaRpcWebsocket.build()
    .uri("wss://api.mainnet-beta.solana.com")
    .webSocketBuilder(httpClient)
    .create();

ws.connect();

PublicKey tokenMint = PublicKey.fromBase58Encoded("MintAddress");
PublicKey owner = PublicKey.fromBase58Encoded("OwnerAddress");

// Subscribe to specific token account
ws.subscribeToTokenAccount(
    tokenMint,
    owner,
    accountInfo -> {
        byte[] data = accountInfo.data();
        TokenAccount account = TokenAccount.read(accountInfo.pubKey(), data);
        System.out.println("Balance updated: " + account.amount());
    }
);

// Subscribe to all token accounts for owner
ws.subscribeToTokenAccounts(
    owner,
    accountInfo -> {
        byte[] data = accountInfo.data();
        TokenAccount account = TokenAccount.read(accountInfo.pubKey(), data);
        System.out.println("Account " + account.mint().toBase58() + 
                         " balance: " + account.amount());
    }
);
Source: SolanaRpcWebsocket.java:160-174

Best Practices

  • Use filters to reduce RPC response size
  • Cache token account addresses when possible
  • Batch multiple account queries together
  • Check for extensions before accessing them
  • Use Token-2022 program ID for new tokens
  • Handle extension data gracefully
  • Use WebSocket subscriptions for real-time updates
  • Implement reconnection logic for WebSockets
  • Fall back to polling if WebSocket fails
  • Use getProgramAccounts with filters instead of fetching all accounts
  • Consider using commitment levels appropriate for your use case
  • Cache mint metadata to reduce RPC calls

Using RPC Client

Learn more about RPC methods and WebSocket subscriptions

Working with Accounts

Understanding Solana accounts and PDAs

Build docs developers (and LLMs) love