Add Wallet Support for Compressed Tokens

Complete guide to add Compressed Token Support to Your Wallet Application.

What you will do

With this guide you will add Compressed Token Support to Your Wallet Application.

Integration Steps

1

Prerequisites

Make sure you have dependencies and developer environment set up!

Prerequisites & Setup

Dependencies

npm install --save-dev typescript tsx @types/node && \
npm install --save \
    @lightprotocol/stateless.js \
    @lightprotocol/compressed-token \
    @solana/web3.js \
    @solana/spl-token

Alternatives:

yarn add --dev typescript tsx @types/node && \
yarn add \
    @lightprotocol/stateless.js \
    @lightprotocol/compressed-token \
    @solana/web3.js \
    @solana/spl-token
pnpm add --save-dev typescript tsx @types/node && \
pnpm add \
    @lightprotocol/stateless.js \
    @lightprotocol/compressed-token \
    @solana/web3.js \
    @solana/spl-token

Developer Environment

By default, this guide uses Localnet.

# Install the development CLI
npm install @lightprotocol/zk-compression-cli
# Start a local test validator
light test-validator
// createRpc() defaults to local test validator endpoints
import {
  Rpc,
  createRpc,
} from "@lightprotocol/stateless.js";

const connection: Rpc = createRpc();

async function main() {
  let slot = await connection.getSlot();
  console.log(slot);

  let health = await connection.getIndexerHealth(slot);
  console.log(health);
  // "Ok"
}

main();

Alternative: Using Devnet

Follow these steps to create an RPC Connection. Replace <your_api_key> with your API key before running.

import { createRpc } from "@lightprotocol/stateless.js";

// Helius exposes Solana and Photon RPC endpoints through a single URL
const RPC_ENDPOINT = "https://devnet.helius-rpc.com?api-key=<your_api_key>";
const connection = createRpc(RPC_ENDPOINT, RPC_ENDPOINT, RPC_ENDPOINT);

console.log("Connection created!");
console.log("RPC Endpoint:", RPC_ENDPOINT);
2

Display Compressed Token Balances

This example fetches and displays all compressed token balances for a wallet address.

import { Rpc, createRpc } from '@lightprotocol/stateless.js';
import { PublicKey } from '@solana/web3.js';

// 1. Setup RPC connection to local test validator  
// 2. Call getCompressedTokenBalancesByOwnerV2() to fetch compressed token balances per mint
// 3. Display results with balance amounts and mint addresses

const connection: Rpc = createRpc(); // defaults to localhost:8899
const publicKey = new PublicKey('CLEuMG7pzJX9xAuKCFzBP154uiG1GaNo4Fq7x6KAcAfG'); // public

(async () => {
    // Fetch compressed token balances for wallet address
    // Returns balance for owner per mint - can optionally apply filter: {mint, limit, cursor}
    const balances = await connection.getCompressedTokenBalancesByOwnerV2(publicKey);
    console.log(balances);
})();
3

Get Transaction History

This example retrieves compression transaction signatures and detailed transaction information for wallet transaction history display.

import { Rpc, createRpc } from '@lightprotocol/stateless.js';
import { PublicKey } from '@solana/web3.js';

// 1. Setup RPC connection and fetch compression transaction signatures using getCompressionSignaturesForOwner()
// 2. Retrieve detailed transaction data with getTransactionWithCompressionInfo() including pre/post balances
// 3. Display transaction history with signature list and balance changes

const connection: Rpc = createRpc(); // defaults to localhost:8899
const publicKey = new PublicKey('CLEuMG7pzJX9xAuKCFzBP154uiG1GaNo4Fq7x6KAcAfG');

(async () => {
    // Fetch compression transaction signatures for wallet address
    // Returns confirmed signatures for compression transactions involving the specified account owner
    const signatures = await connection.getCompressionSignaturesForOwner(publicKey);
    console.log(signatures);

    // Check if any signatures exist before trying to access them
    if (signatures.items.length > 0) {
        // Retrieve detailed transaction information with compression data
        // Returns pre- and post-compressed token balances grouped by owner
        const parsedTransaction = await connection.getTransactionWithCompressionInfo(signatures.items[0].signature);
        console.log(parsedTransaction);
    } else {
        console.log("No compression transactions found for this address");
    }
})();
4

Send Compressed Tokens

First, set up a test mint to and mint 10 compressed tokens to your filesystem wallet.

Set up Test Mint
import { Keypair } from "@solana/web3.js";
import { Rpc, confirmTx, createRpc } from '@lightprotocol/stateless.js';
import { createMint, mintTo } from '@lightprotocol/compressed-token';
import * as fs from 'fs';
import * as os from 'os';

// 1. Setup RPC connection and load filesystem wallet for mint operations
// 2. Call createMint() to create SPL mint with token pool for compression
// 3. Call mintTo() to mint compressed tokens to filesystem wallet

const connection: Rpc = createRpc(); // defaults to localhost:8899

// Load wallet from filesystem
const walletPath = `${os.homedir()}/.config/solana/id.json`;
const secretKey = JSON.parse(fs.readFileSync(walletPath, 'utf8'));
const payer = Keypair.fromSecretKey(Buffer.from(secretKey));
const mintKeypair = Keypair.generate();

(async() => {
    // Fund payer with SOL
    await connection.requestAirdrop(payer.publicKey, 1e9);
    await new Promise(resolve => setTimeout(resolve, 1000));
    
    // Create SPL mint with token pool for compression
    const { mint, transactionSignature } = await createMint(
        connection,
        payer,
        payer.publicKey, // mint authority
        9,
        mintKeypair,
    );
    console.log(`Mint address: ${mint.toString()}`);
    console.log(`Transaction: ${transactionSignature}`);

    // Mint compressed tokens to payer
    const mintToTxId = await mintTo(
        connection,
        payer,
        mint, // SPL mint with token pool for compression
        payer.publicKey, // recipient address
        payer, 
        10e9,
    );

    console.log(`\nMinted ${10e9 / 1e9} compressed token`);
    console.log(`Transaction: ${mintToTxId}`);
})();
send-tokens.ts
// Compressed Token Transfer - Local
// 1. Load wallet and fetch compressed token accounts with getCompressedTokenAccountsByOwner()
// 2. Select accounts for transfer using selectMinCompressedTokenAccountsForTransfer() 
//    and get validity proof with getValidityProof()
// 3. Create transfer instruction with CompressedTokenProgram.transfer() 
//    and submit transaction with sendAndConfirmTx()
// 4. Verify balances via getCompressedTokenAccountsByOwner()

import {
  Rpc,
  createRpc,
  bn,
  dedupeSigner,
  sendAndConfirmTx,
  buildAndSignTx,
} from "@lightprotocol/stateless.js";
import {
  CompressedTokenProgram,
  selectMinCompressedTokenAccountsForTransfer,
} from "@lightprotocol/compressed-token";
import { ComputeBudgetProgram, Keypair, PublicKey } from "@solana/web3.js";
import * as fs from 'fs';
import * as os from 'os';

// Step 1: Setup RPC connection and define transfer parameters
const connection: Rpc = createRpc(); // defaults to localhost:8899
const mint = new PublicKey("MINT ADDRESS"); // Replace with mint address

// Load wallet from filesystem
const walletPath = `${os.homedir()}/.config/solana/id.json`;
const secretKey = JSON.parse(fs.readFileSync(walletPath, 'utf8'));
const payer = Keypair.fromSecretKey(Buffer.from(secretKey));
const owner = payer;

const recipient = Keypair.generate();
const amount = bn(1e8);

(async () => {
  // Step 2: Fetch compressed account hashes from state trees
  const compressedTokenAccounts =
    await connection.getCompressedTokenAccountsByOwner(owner.publicKey, {
      mint, // SPL mint with token pool for compression
    });

  if (compressedTokenAccounts.items.length === 0) {
    console.log("No compressed token accounts found for this mint");
    return;
  }

  // Show initial sender balance
  const initialBalance = compressedTokenAccounts.items.reduce((sum, account) => sum + Number(account.parsed.amount), 0);
  console.log(`Sender balance: ${initialBalance / 1e8} compressed tokens`);

  // Step 3: Select minimum compressed accounts for transfer amount
  const [inputAccounts] = selectMinCompressedTokenAccountsForTransfer(
    compressedTokenAccounts.items,
    amount
  );

  // Get validity proof for Merkle tree verification
  const proof = await connection.getValidityProof(
    inputAccounts.map((account) => account.compressedAccount.hash)
  );

  // Step 4: Create transfer instruction that consumes input accounts and creates new output accounts
  const ix = await CompressedTokenProgram.transfer({
    payer: payer.publicKey,
    inputCompressedTokenAccounts: inputAccounts, // accounts to consume
    toAddress: recipient.publicKey,
    amount,
    recentInputStateRootIndices: proof.rootIndices,
    recentValidityProof: proof.compressedProof,
  });

  // Step 5: Build, sign, and submit transaction
  const { blockhash } = await connection.getLatestBlockhash();
  const additionalSigners = dedupeSigner(payer, [owner]);
  const signedTx = buildAndSignTx(
    [ComputeBudgetProgram.setComputeUnitLimit({ units: 300_000 }), ix],
    payer,
    blockhash,
    additionalSigners
  );
  const transferTxId = await sendAndConfirmTx(connection, signedTx);

  console.log(`\nTransferred ${amount.toNumber() / 1e8} compressed tokens`);
  console.log(`Transaction: ${transferTxId}`);
  console.log(`Recipient: ${recipient.publicKey.toString()}`);

  // Step 6: Verify via getCompressedTokenAccountsByOwner
  const senderCompressedAccounts = await connection.getCompressedTokenAccountsByOwner(payer.publicKey, { mint });
  const senderBalance = senderCompressedAccounts.items.reduce((sum, account) => sum + Number(account.parsed.amount), 0);
  
  const recipientCompressedAccounts = await connection.getCompressedTokenAccountsByOwner(recipient.publicKey, { mint });
  const recipientBalance = recipientCompressedAccounts.items.reduce((sum, account) => sum + Number(account.parsed.amount), 0);

  console.log(`\nSummary compressed token balances:`);
  console.log(`Sender balance: ${senderBalance / 1e8} compressed tokens`);
  console.log(`Recipient balance: ${recipientBalance / 1e8} compressed token`);

  return transferTxId;
})();
5

Success!

You've integrated compressed token support into your wallet!

Your wallet now can

  • Display compressed token balances

  • Show transaction history

  • Send compressed tokens

Advanced Integrations

Use these integrations to let users convert between regular and compressed format as needed.

Decompress to Regular SPL

This example converts compressed tokens to regular SPL format using CompressedTokenProgram.decompress().

import {
  bn,
  buildAndSignTx,
  sendAndConfirmTx,
  dedupeSigner,
  Rpc,
  createRpc,
} from "@lightprotocol/stateless.js";
import { ComputeBudgetProgram } from "@solana/web3.js";
import {
  CompressedTokenProgram,
  getTokenPoolInfos,
  selectMinCompressedTokenAccountsForTransfer,
  selectTokenPoolInfosForDecompression,
} from "@lightprotocol/compressed-token";

// 1. Setup RPC connection and fetch compressed token accounts with getCompressedTokenAccountsByOwner()
// 2. Select accounts and token pool infos using selectMinCompressedTokenAccountsForTransfer() and selectTokenPoolInfosForDecompression()
// 3. Create decompress instruction with CompressedTokenProgram.decompress() and submit transaction

// Step 1: Setup RPC connection and define decompression parameters
const connection: Rpc = createRpc("https://mainnet.helius-rpc.com?api-key=<api_key>";);
const payer = PAYER_KEYPAIR;
const owner = PAYER_KEYPAIR;
const mint = MINT_ADDRESS;
const amount = 1e5; // 100K tokens to decompress

(async () => {
  // 1. Fetch compressed token accounts
  const compressedTokenAccounts =
    await connection.getCompressedTokenAccountsByOwner(owner.publicKey, {
      mint,
    });

  // 2. Select
  const [inputAccounts] = selectMinCompressedTokenAccountsForTransfer(
    compressedTokenAccounts.items,
    bn(amount)
  );

  // 3. Fetch validity proof
  const proof = await connection.getValidityProof(
    inputAccounts.map((account) => account.compressedAccount.hash)
  );

  // 4. Fetch & Select tokenPoolInfos
  const tokenPoolInfos = await getTokenPoolInfos(connection, mint);
  const selectedTokenPoolInfos = selectTokenPoolInfosForDecompression(
    tokenPoolInfos,
    amount
  );

  // 5. Build instruction
  const ix = await CompressedTokenProgram.decompress({
    payer: payer.publicKey,
    inputCompressedTokenAccounts: inputAccounts,
    toAddress: owner.publicKey,
    amount,
    tokenPoolInfos: selectedTokenPoolInfos,
    recentInputStateRootIndices: proof.rootIndices,
    recentValidityProof: proof.compressedProof,
  });
  
  
  // 6. Sign, send, and confirm.
  // Example with keypair:
  const { blockhash } = await connection.getLatestBlockhash();
  const additionalSigners = dedupeSigner(payer, [owner]);
  const signedTx = buildAndSignTx(
    [ComputeBudgetProgram.setComputeUnitLimit({ units: 300_000 }), ix],
    payer,
    blockhash,
    additionalSigners
  );

  return await sendAndConfirmTx(connection, signedTx);
})();
Compress Regular SPL Tokens

This example converts regular SPL tokens to compressed format using CompressedTokenProgram.compress().

// 1. Setup RPC connection and get user ATA with getOrCreateAssociatedTokenAccount()
// 2. Fetch state tree and token pool infos using getStateTreeInfos() and getTokenPoolInfos()
// 3. Create compress instruction with CompressedTokenProgram.compress() and submit transaction


import {
  buildAndSignTx,
  sendAndConfirmTx,
  Rpc,
  createRpc,
  selectStateTreeInfo,
} from "@lightprotocol/stateless.js";
import { ComputeBudgetProgram } from "@solana/web3.js";
import {
  CompressedTokenProgram,
  getTokenPoolInfos,
  selectTokenPoolInfo,
} from "@lightprotocol/compressed-token";
import { getOrCreateAssociatedTokenAccount } from "@solana/spl-token";

// Step 1: Setup RPC connection and define compression parameters
const connection: Rpc = createRpc(
  "https://mainnet.helius-rpc.com?api-key=<api_key>"
);
  const payer = <PAYER_KEYPAIR>;
  const mint = <MINT_ADDRESS>;
const amount = 1e5; // 100K tokens to compress

(async () => {
  // Step 2: Get or create associated token account for SPL tokens
  const sourceTokenAccount = await getOrCreateAssociatedTokenAccount(
    connection,
    payer, // fee payer
    mint, // token mint address
    payer.publicKey // token account owner
  );

  // Step 3: Fetch and select state tree info for compression
  const treeInfos = await connection.getStateTreeInfos();
  const treeInfo = selectStateTreeInfo(treeInfos);

  // Step 4: Fetch and select token pool info for compression
  const tokenPoolInfos = await getTokenPoolInfos(connection, mint);
  const tokenPoolInfo = selectTokenPoolInfo(tokenPoolInfos);

  // Step 5: Create compress instruction - transfer SPL tokens to pool and create compressed accounts
  const compressInstruction = await CompressedTokenProgram.compress({
    payer: payer.publicKey, // fee payer
    owner: payer.publicKey, // owner of source SPL tokens
    source: sourceTokenAccount.address, // source ATA address
    toAddress: payer.publicKey, // recipient of compressed tokens (self)
    amount, // amount to compress
    mint, // token mint address
    outputStateTreeInfo: treeInfo, // state tree for compressed accounts
    tokenPoolInfo, // token pool for compression
  });

  // Step 6: Build, sign, and submit compression transaction
  const { blockhash } = await connection.getLatestBlockhash();
  const tx = buildAndSignTx(
    [
      ComputeBudgetProgram.setComputeUnitLimit({ units: 300_000 }),
      compressInstruction,
    ],
    payer, // transaction signer
    blockhash,
    [payer] // additional signers
  );
  await sendAndConfirmTx(connection, tx);
})();

Common Errors

No compressed tokens found

If getCompressedTokenBalancesByOwnerV2 returns empty:

  • Ensure the wallet has compressed tokens (not regular SPL tokens)

  • Verify you're on the correct network (devnet/mainnet)

Best Practices

  • Clear UI Indicators — Provide clear visual distinctions between compressed and uncompressed SPL tokens

  • Transaction History — Provide detailed transaction histories for compressed tokens

  • Decompression and Compression — Provide a clear path for users to convert between compressed and uncompressed tokens when needed


Next Steps

Explore more guides in our cookbook section.

Cookbook

Last updated