| Creation Cost | SPL Token Account | Compressed Token |
|---|---|---|
| Per account | ~2,000,000 lamports | ~5,000 lamports |
- Authenticate with Privy
- Build unsigned transaction
- Sign transaction using Privy’s wallet provider
- Send signed transaction to RPC
What you will implement
| SPL | Compressed | |
|---|---|---|
| Transfer | transferChecked() | transfer() |
| Compress | N/A | compress() |
| Decompress | N/A | decompress() |
| Get Balance | getAccount() | getCompressedTokenAccountsByOwner() |
| Transaction History | getSignaturesForAddress() | getCompressionSignaturesForOwner() |
- Guide
- AI Prompt
1
Prerequisites
Report incorrect code
Copy
Ask AI
npm install @lightprotocol/compressed-token \
@lightprotocol/stateless.js
Report incorrect code
Copy
Ask AI
import { createRpc } from "@lightprotocol/stateless.js";
import {
transfer,
compress,
decompress,
selectMinCompressedTokenAccountsForTransfer,
} from "@lightprotocol/compressed-token";
const rpc = createRpc(RPC_ENDPOINT);
Setup test mint
Setup test mint
Before we call compress or decompresss, we need:
- An SPL mint with a interface PDA for compression. This PDA can be created for new SPL mints via
createMint()or added to existing SPL mints viacreateTokenPool(). - For
compress()SPL tokens in an Associated Token Account, or - For
decompress()compressed token accounts with sufficient balance.
Report incorrect code
Copy
Ask AI
import "dotenv/config";
import { Keypair } from "@solana/web3.js";
import { createRpc } from "@lightprotocol/stateless.js";
import { createMint } from "@lightprotocol/compressed-token";
import { homedir } from "os";
import { readFileSync } from "fs";
// devnet:
const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`;
// localnet:
// const RPC_URL = undefined;
const payer = Keypair.fromSecretKey(
new Uint8Array(
JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8"))
)
);
(async function () {
// devnet:
const rpc = createRpc(RPC_URL);
// localnet:
// const rpc = createRpc();
const { mint, transactionSignature } = await createMint(
rpc,
payer,
payer.publicKey,
9
);
console.log("Mint:", mint.toBase58());
console.log("Tx:", transactionSignature);
})();
2
Full Code Examples
- Compressed Transfer
- Compress SPL to Recipient
- Decompress (Offramp)
Send compressed tokens to another recipient, similar to SPL token transfers:
- Fetch compressed token accounts with
getCompressedTokenAccountsByOwner - Select sender accounts with
selectMinCompressedTokenAccountsForTransfer - Fetch a validity proof from your RPC provider to prove the account’s state correctness
- Build the transfer instruction and transaction
- Sign with Privy and send transaction
- Node.js
- React
Report incorrect code
Copy
Ask AI
import 'dotenv/config';
import {PrivyClient} from '@privy-io/node';
import {createRpc, bn} from '@lightprotocol/stateless.js';
import {PublicKey, Transaction, ComputeBudgetProgram} from '@solana/web3.js';
import {CompressedTokenProgram, selectMinCompressedTokenAccountsForTransfer} from '@lightprotocol/compressed-token';
const transferCompressedTokens = async (
fromAddress: string,
toAddress: string,
tokenMintAddress: string,
amount: number,
decimals: number = 6
) => {
const connection = createRpc(process.env.HELIUS_RPC_URL!);
const privy = new PrivyClient({
appId: process.env.PRIVY_APP_ID!,
appSecret: process.env.PRIVY_APP_SECRET!,
});
// Create public key objects
const fromPubkey = new PublicKey(fromAddress);
const toPubkey = new PublicKey(toAddress);
const mintPubkey = new PublicKey(tokenMintAddress);
const tokenAmount = bn(amount * Math.pow(10, decimals));
// Get compressed token accounts (filter out null items from indexer)
const accounts = await connection.getCompressedTokenAccountsByOwner(fromPubkey, {mint: mintPubkey});
const validItems = (accounts.items || []).filter((item): item is NonNullable<typeof item> => item !== null);
if (validItems.length === 0) {
throw new Error('No compressed token accounts found');
}
// Select minimum accounts needed for transfer
const [inputAccounts] = selectMinCompressedTokenAccountsForTransfer(validItems, tokenAmount);
if (inputAccounts.length === 0) {
throw new Error('Insufficient balance');
}
// Get validity proof to prove compressed token accounts exist in state tree.
const proof = await connection.getValidityProof(inputAccounts.map(account => bn(account.compressedAccount.hash)));
// Create transfer instruction
const instruction = await CompressedTokenProgram.transfer({
payer: fromPubkey,
inputCompressedTokenAccounts: inputAccounts,
toAddress: toPubkey,
amount: tokenAmount,
recentInputStateRootIndices: proof.rootIndices,
recentValidityProof: proof.compressedProof,
});
// Create transaction
const transaction = new Transaction();
transaction.add(ComputeBudgetProgram.setComputeUnitLimit({units: 300_000}));
transaction.add(instruction);
// Get recent blockhash
const {blockhash} = await connection.getLatestBlockhash();
transaction.recentBlockhash = blockhash;
transaction.feePayer = fromPubkey;
// Sign with Privy
const signResult = await privy.wallets().solana().signTransaction(process.env.TREASURY_WALLET_ID!, {
transaction: transaction.serialize({requireAllSignatures: false}),
authorization_context: {
authorization_private_keys: [process.env.TREASURY_AUTHORIZATION_KEY!]
}
});
const signedTx = (signResult as any).signed_transaction || signResult.signedTransaction;
if (!signedTx) {
throw new Error('Privy returned invalid response: ' + JSON.stringify(signResult));
}
const signedTransaction = Buffer.from(signedTx, 'base64');
// Send transaction
const signature = await connection.sendRawTransaction(signedTransaction, {
skipPreflight: false,
preflightCommitment: 'confirmed'
});
await connection.confirmTransaction(signature, 'confirmed');
return signature;
};
export default transferCompressedTokens;```
Report incorrect code
Copy
Ask AI
import { useState } from 'react';
import { PublicKey, Transaction, ComputeBudgetProgram } from '@solana/web3.js';
import { CompressedTokenProgram, selectMinCompressedTokenAccountsForTransfer } from '@lightprotocol/compressed-token';
import { bn, createRpc } from '@lightprotocol/stateless.js';
import type { ConnectedStandardSolanaWallet } from '@privy-io/js-sdk-core';
import type { SignTransactionResult } from '@privy-io/react-auth/solana';
export interface TransferParams {
ownerPublicKey: string;
mint: string;
toAddress: string;
amount: number;
decimals?: number;
}
export interface TransferArgs {
params: TransferParams;
wallet: ConnectedStandardSolanaWallet;
signTransaction: (args: {
transaction: Buffer;
wallet: ConnectedStandardSolanaWallet;
chain: string;
}) => Promise<SignTransactionResult>;
}
export function useTransfer() {
const [isLoading, setIsLoading] = useState(false);
const transfer = async (args: TransferArgs): Promise<string> => {
setIsLoading(true);
try {
const { params, wallet, signTransaction } = args;
const { ownerPublicKey, mint, toAddress, amount, decimals = 6 } = params;
// Create RPC connection
const rpc = createRpc(import.meta.env.VITE_HELIUS_RPC_URL);
const owner = new PublicKey(ownerPublicKey);
const mintPubkey = new PublicKey(mint);
const recipient = new PublicKey(toAddress);
const tokenAmount = bn(Math.floor(amount * Math.pow(10, decimals)));
// Get compressed token accounts
const accounts = await rpc.getCompressedTokenAccountsByOwner(owner, { mint: mintPubkey });
if (!accounts.items || accounts.items.length === 0) {
throw new Error('No compressed token accounts found');
}
// Select minimum accounts needed
const [inputAccounts] = selectMinCompressedTokenAccountsForTransfer(accounts.items, tokenAmount);
if (inputAccounts.length === 0) {
throw new Error('Insufficient balance');
}
// Get validity proof
const proof = await rpc.getValidityProof(
inputAccounts.map((account) => bn(account.compressedAccount.hash))
);
// Build transfer instruction
const transferIx = await CompressedTokenProgram.transfer({
payer: owner,
inputCompressedTokenAccounts: inputAccounts,
toAddress: recipient,
amount: tokenAmount,
recentInputStateRootIndices: proof.rootIndices,
recentValidityProof: proof.compressedProof,
});
// Build transaction
const { blockhash } = await rpc.getLatestBlockhash();
const transaction = new Transaction();
transaction.add(ComputeBudgetProgram.setComputeUnitLimit({ units: 300_000 }));
transaction.add(transferIx);
transaction.recentBlockhash = blockhash;
transaction.feePayer = owner;
// Serialize unsigned transaction
const unsignedTxBuffer = transaction.serialize({ requireAllSignatures: false });
// Sign with Privy
const signedTx = await signTransaction({
transaction: unsignedTxBuffer,
wallet,
chain: 'solana:devnet',
});
// Send transaction
const signedTxBuffer = Buffer.from(signedTx.signedTransaction);
const signature = await rpc.sendRawTransaction(signedTxBuffer, {
skipPreflight: false,
preflightCommitment: 'confirmed',
});
return signature;
} finally {
setIsLoading(false);
}
};
return { transfer, isLoading };
}
Convert SPL to compressed tokens and send to a recipient in one instruction.
- Node.js
- React
Report incorrect code
Copy
Ask AI
import 'dotenv/config';
import {PrivyClient} from '@privy-io/node';
import {createRpc, bn, selectStateTreeInfo} from '@lightprotocol/stateless.js';
import {PublicKey, Transaction, ComputeBudgetProgram} from '@solana/web3.js';
import {getAssociatedTokenAddressSync, getAccount} from '@solana/spl-token';
import {CompressedTokenProgram, getTokenPoolInfos, selectTokenPoolInfo} from '@lightprotocol/compressed-token';
const compressTokens = async (
fromAddress: string,
toAddress: string,
tokenMintAddress: string,
amount: number,
decimals: number = 6
) => {
const connection = createRpc(process.env.HELIUS_RPC_URL!);
const privy = new PrivyClient({
appId: process.env.PRIVY_APP_ID!,
appSecret: process.env.PRIVY_APP_SECRET!,
});
// Create public key objects
const fromPubkey = new PublicKey(fromAddress);
const toPubkey = new PublicKey(toAddress);
const mintPubkey = new PublicKey(tokenMintAddress);
const tokenAmount = bn(amount * Math.pow(10, decimals));
// Get source token account and verify balance
const ownerAta = getAssociatedTokenAddressSync(mintPubkey, fromPubkey);
const ataAccount = await getAccount(connection, ownerAta);
if (ataAccount.amount < BigInt(tokenAmount.toString())) {
throw new Error('Insufficient SPL balance');
}
// Get state tree to store compressed tokens
// Get token pool info. Stores SPL tokens in interface PDA when compressed.
const stateTreeInfos = await connection.getStateTreeInfos();
const selectedTreeInfo = selectStateTreeInfo(stateTreeInfos);
const tokenPoolInfos = await getTokenPoolInfos(connection, mintPubkey);
const tokenPoolInfo = selectTokenPoolInfo(tokenPoolInfos);
// Create compress instruction
const instruction = await CompressedTokenProgram.compress({
payer: fromPubkey,
owner: fromPubkey,
source: ownerAta,
toAddress: toPubkey,
mint: mintPubkey,
amount: tokenAmount,
outputStateTreeInfo: selectedTreeInfo,
tokenPoolInfo,
});
// Create transaction
const transaction = new Transaction();
transaction.add(ComputeBudgetProgram.setComputeUnitLimit({units: 300_000}));
transaction.add(instruction);
// Get recent blockhash
const {blockhash} = await connection.getLatestBlockhash();
transaction.recentBlockhash = blockhash;
transaction.feePayer = fromPubkey;
// Sign with Privy
const signResult = await privy.wallets().solana().signTransaction(process.env.TREASURY_WALLET_ID!, {
transaction: transaction.serialize({requireAllSignatures: false}),
authorization_context: {
authorization_private_keys: [process.env.TREASURY_AUTHORIZATION_KEY!]
}
});
const signedTx = (signResult as any).signed_transaction || signResult.signedTransaction;
if (!signedTx) {
throw new Error('Privy returned invalid response: ' + JSON.stringify(signResult));
}
const signedTransaction = Buffer.from(signedTx, 'base64');
// Send transaction
const signature = await connection.sendRawTransaction(signedTransaction, {
skipPreflight: false,
preflightCommitment: 'confirmed'
});
await connection.confirmTransaction(signature, 'confirmed');
return signature;
};
export default compressTokens;
Report incorrect code
Copy
Ask AI
import { useState } from 'react';
import { PublicKey, Transaction, ComputeBudgetProgram } from '@solana/web3.js';
import { getAssociatedTokenAddressSync, getAccount } from '@solana/spl-token';
import { bn, createRpc, selectStateTreeInfo } from '@lightprotocol/stateless.js';
import { CompressedTokenProgram, getTokenPoolInfos, selectTokenPoolInfo } from '@lightprotocol/compressed-token';
import type { ConnectedStandardSolanaWallet } from '@privy-io/js-sdk-core';
import type { SignTransactionResult } from '@privy-io/react-auth/solana';
export interface CompressParams {
ownerPublicKey: string;
toAddress: string;
mint: string;
amount: number;
decimals?: number;
}
export interface CompressArgs {
params: CompressParams;
wallet: ConnectedStandardSolanaWallet;
signTransaction: (args: {
transaction: Buffer;
wallet: ConnectedStandardSolanaWallet;
chain: string;
}) => Promise<SignTransactionResult>;
}
export function useCompress() {
const [isLoading, setIsLoading] = useState(false);
const compress = async (args: CompressArgs): Promise<string> => {
setIsLoading(true);
try {
const { params, wallet, signTransaction } = args;
const { ownerPublicKey, toAddress, mint, amount, decimals = 6 } = params;
// Create RPC connection
const rpc = createRpc(import.meta.env.VITE_HELIUS_RPC_URL);
// Create public key objects
const ownerPubkey = new PublicKey(ownerPublicKey);
const toPubkey = new PublicKey(toAddress);
const mintPubkey = new PublicKey(mint);
const tokenAmount = bn(amount * Math.pow(10, decimals));
// Get source token account and verify balance
const ownerAta = getAssociatedTokenAddressSync(mintPubkey, ownerPubkey);
const ataAccount = await getAccount(rpc, ownerAta);
if (ataAccount.amount < BigInt(tokenAmount.toString())) {
throw new Error('Insufficient SPL balance');
}
// Get state tree to store compressed tokens
// Get token pool info. Stores SPL tokens in interface PDA when compressed.
const stateTreeInfos = await rpc.getStateTreeInfos();
const selectedTreeInfo = selectStateTreeInfo(stateTreeInfos);
const tokenPoolInfos = await getTokenPoolInfos(rpc, mintPubkey);
const tokenPoolInfo = selectTokenPoolInfo(tokenPoolInfos);
// Create compress instruction
const instruction = await CompressedTokenProgram.compress({
payer: ownerPubkey,
owner: ownerPubkey,
source: ownerAta,
toAddress: toPubkey,
mint: mintPubkey,
amount: tokenAmount,
outputStateTreeInfo: selectedTreeInfo,
tokenPoolInfo,
});
// Build transaction
const { blockhash } = await rpc.getLatestBlockhash();
const transaction = new Transaction();
transaction.add(ComputeBudgetProgram.setComputeUnitLimit({ units: 300_000 }));
transaction.add(instruction);
transaction.recentBlockhash = blockhash;
transaction.feePayer = ownerPubkey;
// Serialize unsigned transaction
const unsignedTxBuffer = transaction.serialize({ requireAllSignatures: false });
// Sign with Privy
const signedTx = await signTransaction({
transaction: unsignedTxBuffer,
wallet,
chain: 'solana:devnet',
});
// Send transaction
const signedTxBuffer = Buffer.from(signedTx.signedTransaction);
const signature = await rpc.sendRawTransaction(signedTxBuffer, {
skipPreflight: false,
preflightCommitment: 'confirmed',
});
return signature;
} finally {
setIsLoading(false);
}
};
return { compress, isLoading };
}
Convert compressed tokens back to SPL tokens.
- Node.js
- React
Report incorrect code
Copy
Ask AI
import 'dotenv/config';
import {PrivyClient} from '@privy-io/node';
import {createRpc} from '@lightprotocol/stateless.js';
import {Keypair, PublicKey, Transaction} from '@solana/web3.js';
import {getAssociatedTokenAddressSync, createAssociatedTokenAccount} from '@solana/spl-token';
import {decompress} from '@lightprotocol/compressed-token';
const decompressTokens = async (
fromAddress: string,
tokenMintAddress: string,
amount: number,
decimals: number = 6
) => {
const connection = createRpc(process.env.HELIUS_RPC_URL!);
const privy = new PrivyClient({
appId: process.env.PRIVY_APP_ID!,
appSecret: process.env.PRIVY_APP_SECRET!,
});
const fromPubkey = new PublicKey(fromAddress);
const mintPubkey = new PublicKey(tokenMintAddress);
const rawAmount = amount * Math.pow(10, decimals);
// Get destination ATA
const ownerAta = getAssociatedTokenAddressSync(mintPubkey, fromPubkey);
// Check ATA exists (decompress action will handle creation internally)
// But we need to be aware ATA creation requires a separate signer
// Create fake keypair for decompress action (only publicKey is used)
const dummyPayer = {
publicKey: fromPubkey,
secretKey: new Uint8Array(64),
} as any;
// Intercept sendAndConfirmTransaction to use Privy signing
const originalSendAndConfirm = (connection as any).sendAndConfirmTransaction;
(connection as any).sendAndConfirmTransaction = async (tx: Transaction, signers: any[]) => {
const signResult = await privy.wallets().solana().signTransaction(process.env.TREASURY_WALLET_ID!, {
transaction: tx.serialize({requireAllSignatures: false}),
authorization_context: {
authorization_private_keys: [process.env.TREASURY_AUTHORIZATION_KEY!]
}
});
const signedTx = (signResult as any).signed_transaction || signResult.signedTransaction;
if (!signedTx) {
throw new Error('Privy returned invalid response');
}
const signedTransaction = Buffer.from(signedTx, 'base64');
const signature = await connection.sendRawTransaction(signedTransaction, {
skipPreflight: false,
preflightCommitment: 'confirmed'
});
await connection.confirmTransaction(signature, 'confirmed');
return signature;
};
try {
// Use high-level decompress action (handles account configuration correctly)
const signature = await decompress(
connection,
dummyPayer,
mintPubkey,
rawAmount,
dummyPayer,
ownerAta
);
return signature;
} finally {
// Restore original function
(connection as any).sendAndConfirmTransaction = originalSendAndConfirm;
}
};
export default decompressTokens;
Report incorrect code
Copy
Ask AI
import { useState } from 'react';
import { PublicKey, Transaction, ComputeBudgetProgram } from '@solana/web3.js';
import { getAssociatedTokenAddressSync, createAssociatedTokenAccountInstruction, getAccount, TokenAccountNotFoundError } from '@solana/spl-token';
import { CompressedTokenProgram, selectMinCompressedTokenAccountsForTransfer, getSplInterfaceInfos, selectSplInterfaceInfosForDecompression } from '@lightprotocol/compressed-token';
import { bn, createRpc } from '@lightprotocol/stateless.js';
import type { ConnectedStandardSolanaWallet } from '@privy-io/js-sdk-core';
import type { SignTransactionResult } from '@privy-io/react-auth/solana';
export interface DecompressParams {
ownerPublicKey: string;
mint: string;
amount: number;
decimals?: number;
}
export interface DecompressArgs {
params: DecompressParams;
wallet: ConnectedStandardSolanaWallet;
signTransaction: (args: {
transaction: Buffer;
wallet: ConnectedStandardSolanaWallet;
chain: string;
}) => Promise<SignTransactionResult>;
}
export function useDecompress() {
const [isLoading, setIsLoading] = useState(false);
const decompress = async (args: DecompressArgs): Promise<string> => {
setIsLoading(true);
try {
const { params, wallet, signTransaction } = args;
const { ownerPublicKey, mint, amount, decimals = 6 } = params;
// Create RPC connection
const rpc = createRpc(import.meta.env.VITE_HELIUS_RPC_URL);
const owner = new PublicKey(ownerPublicKey);
const mintPubkey = new PublicKey(mint);
const tokenAmount = bn(Math.floor(amount * Math.pow(10, decimals)));
// Get destination ATA
const destinationAta = getAssociatedTokenAddressSync(mintPubkey, owner);
// Get compressed token accounts
const accounts = await rpc.getCompressedTokenAccountsByOwner(owner, { mint: mintPubkey });
if (!accounts.items || accounts.items.length === 0) {
throw new Error('No compressed token accounts found');
}
// Select minimum accounts needed
const [inputAccounts] = selectMinCompressedTokenAccountsForTransfer(accounts.items, tokenAmount);
if (inputAccounts.length === 0) {
throw new Error('Insufficient compressed balance');
}
// Get validity proof and token pool info
const proof = await rpc.getValidityProof(
inputAccounts.map((account) => bn(account.compressedAccount.hash))
);
const splInterfaceInfos = await getSplInterfaceInfos(rpc, mintPubkey);
const tokenPoolInfos = selectSplInterfaceInfosForDecompression(splInterfaceInfos, tokenAmount);
// Build transaction
const { blockhash } = await rpc.getLatestBlockhash();
const transaction = new Transaction();
transaction.add(ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 }));
// Create ATA if needed
try {
await getAccount(rpc, destinationAta);
} catch (e) {
if (e instanceof TokenAccountNotFoundError) {
transaction.add(
createAssociatedTokenAccountInstruction(owner, destinationAta, owner, mintPubkey)
);
} else {
throw e;
}
}
// Build decompress instruction
const decompressIx = await CompressedTokenProgram.decompress({
payer: owner,
inputCompressedTokenAccounts: inputAccounts,
toAddress: destinationAta,
amount: tokenAmount,
recentInputStateRootIndices: proof.rootIndices,
recentValidityProof: proof.compressedProof,
tokenPoolInfos,
});
transaction.add(decompressIx);
transaction.recentBlockhash = blockhash;
transaction.feePayer = owner;
// Serialize unsigned transaction
const unsignedTxBuffer = transaction.serialize({ requireAllSignatures: false });
// Sign with Privy
const signedTx = await signTransaction({
transaction: unsignedTxBuffer,
wallet,
chain: 'solana:devnet',
});
// Send transaction
const signedTxBuffer = Buffer.from(signedTx.signedTransaction);
const signature = await rpc.sendRawTransaction(signedTxBuffer, {
skipPreflight: false,
preflightCommitment: 'confirmed',
});
return signature;
} finally {
setIsLoading(false);
}
};
return { decompress, isLoading };
}
Get balances
Fetch SPL and compressed token balances.- Node.js
- React
Report incorrect code
Copy
Ask AI
import {PublicKey} from '@solana/web3.js';
import {createRpc} from '@lightprotocol/stateless.js';
import {HELIUS_RPC_URL} from './config.js';
export async function getCompressedBalances(ownerAddress: string) {
const rpc = createRpc(HELIUS_RPC_URL);
const owner = new PublicKey(ownerAddress);
// Get compressed SOL balance
const compressedSol = await rpc.getCompressedBalanceByOwner(owner);
const compressedSolLamports = compressedSol.value ? BigInt(compressedSol.value.toString()) : 0n;
// Get compressed token accounts (filter out null items from indexer)
const compressedAccounts = await rpc.getCompressedTokenAccountsByOwner(owner);
const validItems = (compressedAccounts.items || []).filter((item): item is NonNullable<typeof item> => item !== null);
// Aggregate balances by mint
const balances = new Map<string, bigint>();
for (const account of validItems) {
if (account.parsed) {
const mint = account.parsed.mint.toBase58();
const amount = BigInt(account.parsed.amount.toString());
const current = balances.get(mint) || 0n;
balances.set(mint, current + amount);
}
}
return {
sol: compressedSolLamports.toString(),
tokens: Array.from(balances.entries()).map(([mint, amount]) => ({
mint,
amount: amount.toString(),
accounts: validItems.filter(a => a.parsed?.mint.toBase58() === mint).length
}))
};
}
Report incorrect code
Copy
Ask AI
import { useState, useCallback } from 'react';
import { PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js';
import { getAccount, getAssociatedTokenAddressSync } from '@solana/spl-token';
import { createRpc } from '@lightprotocol/stateless.js';
const USDC_MINT = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
const TEST_MINT = '7cT3PeXyDLkyEcvr9YzsjGLuZneKsea4c8hPbJQEjMCZ';
export interface TokenBalance {
mint: string;
amount: string;
decimals: number;
isCompressed: boolean;
isNative: boolean;
}
export function useCompressedBalances() {
const [balances, setBalances] = useState<TokenBalance[]>([]);
const [isLoading, setIsLoading] = useState(false);
const fetchBalances = useCallback(async (ownerAddress: string) => {
if (!ownerAddress) return;
setIsLoading(true);
try {
const rpc = createRpc(import.meta.env.VITE_HELIUS_RPC_URL);
const owner = new PublicKey(ownerAddress);
const allBalances: TokenBalance[] = [];
// Get compressed SOL balance
try {
const compressedSol = await rpc.getCompressedBalanceByOwner(owner);
if (compressedSol && BigInt(compressedSol.toString()) > 0n) {
allBalances.push({
mint: 'So11111111111111111111111111111111111111112',
amount: compressedSol.toString(),
decimals: 9,
isCompressed: true,
isNative: true,
});
}
} catch {
// No compressed SOL
}
// Get regular SOL balance
try {
const solBalance = await rpc.getBalance(owner);
allBalances.push({
mint: 'So11111111111111111111111111111111111111112',
amount: solBalance.toString(),
decimals: 9,
isCompressed: false,
isNative: true,
});
} catch {
// Failed to get SOL balance
}
// Get regular SPL token accounts
try {
const tokenAccounts = await rpc.getTokenAccountsByOwner(owner, {
programId: new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'),
});
for (const { account, pubkey } of tokenAccounts.value) {
const data = account.data;
if (data instanceof Buffer || (typeof data === 'object' && 'length' in data)) {
// Parse token account data
const dataBuffer = Buffer.from(data as Uint8Array);
if (dataBuffer.length >= 72) {
const mint = new PublicKey(dataBuffer.subarray(0, 32));
const amount = dataBuffer.readBigUInt64LE(64);
// Get mint info for decimals
try {
const mintInfo = await rpc.getAccountInfo(mint);
const decimals = mintInfo?.data ? (mintInfo.data as Buffer)[44] || 6 : 6;
allBalances.push({
mint: mint.toBase58(),
amount: amount.toString(),
decimals,
isCompressed: false,
isNative: false,
});
} catch {
allBalances.push({
mint: mint.toBase58(),
amount: amount.toString(),
decimals: 6,
isCompressed: false,
isNative: false,
});
}
}
}
}
} catch {
// Failed to get token accounts
}
// Get compressed token accounts
try {
const compressedAccounts = await rpc.getCompressedTokenAccountsByOwner(owner);
if (compressedAccounts?.items) {
const mintAmounts = new Map<string, bigint>();
const mintDecimals = new Map<string, number>();
for (const item of compressedAccounts.items) {
if (item?.parsed?.mint && item?.parsed?.amount) {
const mint = item.parsed.mint.toBase58();
const amount = BigInt(item.parsed.amount.toString());
const current = mintAmounts.get(mint) || 0n;
mintAmounts.set(mint, current + amount);
// Store decimals (assume 6 for now, could fetch from mint)
if (!mintDecimals.has(mint)) {
mintDecimals.set(mint, 6);
}
}
}
for (const [mint, amount] of mintAmounts) {
allBalances.push({
mint,
amount: amount.toString(),
decimals: mintDecimals.get(mint) || 6,
isCompressed: true,
isNative: false,
});
}
}
} catch {
// Failed to get compressed accounts
}
// Add fallback tokens with 0 balance if not present
const hasMint = (mint: string) => allBalances.some(b => b.mint === mint);
if (!hasMint(USDC_MINT)) {
allBalances.push({
mint: USDC_MINT,
amount: '0',
decimals: 6,
isCompressed: false,
isNative: false,
});
}
if (!hasMint(TEST_MINT)) {
allBalances.push({
mint: TEST_MINT,
amount: '0',
decimals: 6,
isCompressed: false,
isNative: false,
});
}
setBalances(allBalances);
} catch (error) {
console.error('Failed to fetch balances:', error);
setBalances([]);
} finally {
setIsLoading(false);
}
}, []);
return { balances, isLoading, fetchBalances };
}
Get transaction history
Fetch compressed token transaction history for an owner, with optional detailed compression information.- Node.js
- React
Report incorrect code
Copy
Ask AI
import 'dotenv/config';
import {createRpc} from '@lightprotocol/stateless.js';
import {PublicKey} from '@solana/web3.js';
const getTransactionHistory = async (
ownerAddress: string,
limit: number = 10,
includeDetails: boolean = false
) => {
const connection = createRpc(process.env.HELIUS_RPC_URL!);
const owner = new PublicKey(ownerAddress);
// Get compression signatures for token owner
const signatures = await connection.getCompressionSignaturesForTokenOwner(owner);
if (signatures.items.length === 0) {
return {
count: 0,
transactions: [],
};
}
// Limit results
const limitedSignatures = signatures.items.slice(0, limit);
// Get detailed info if requested
if (includeDetails && limitedSignatures.length > 0) {
const transactions = await Promise.all(
limitedSignatures.map(async (sig) => {
const txInfo = await connection.getTransactionWithCompressionInfo(sig.signature);
return {
signature: sig.signature,
slot: sig.slot,
blockTime: sig.blockTime,
timestamp: new Date(sig.blockTime * 1000).toISOString(),
compressionInfo: txInfo?.compressionInfo ? {
closedAccounts: txInfo.compressionInfo.closedAccounts.length,
openedAccounts: txInfo.compressionInfo.openedAccounts.length,
} : null,
};
})
);
return {
count: signatures.items.length,
transactions,
};
}
// Return basic signature info
const transactions = limitedSignatures.map((sig) => ({
signature: sig.signature,
slot: sig.slot,
blockTime: sig.blockTime,
timestamp: new Date(sig.blockTime * 1000).toISOString(),
}));
return {
count: signatures.items.length,
transactions,
};
};
export default getTransactionHistory;
Report incorrect code
Copy
Ask AI
import { useState, useCallback } from 'react';
import { PublicKey } from '@solana/web3.js';
import { createRpc } from '@lightprotocol/stateless.js';
export interface TransactionCompressionInfo {
closedAccounts: number;
openedAccounts: number;
}
export interface Transaction {
signature: string;
slot: number;
blockTime: number;
timestamp: string;
compressionInfo?: TransactionCompressionInfo | null;
}
export function useTransactionHistory() {
const [transactions, setTransactions] = useState<Transaction[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchTransactionHistory = useCallback(
async (
ownerAddress: string,
limit: number = 10,
includeDetails: boolean = false
) => {
if (!ownerAddress) {
setTransactions([]);
return;
}
setIsLoading(true);
setError(null);
try {
const rpc = createRpc(import.meta.env.VITE_HELIUS_RPC_URL);
const owner = new PublicKey(ownerAddress);
const signatures = await rpc.getCompressionSignaturesForTokenOwner(owner);
if (signatures.items.length === 0) {
setTransactions([]);
return;
}
const limitedSignatures = signatures.items.slice(0, limit);
if (includeDetails && limitedSignatures.length > 0) {
const transactionsWithDetails = await Promise.all(
limitedSignatures.map(async (sig) => {
const txInfo = await rpc.getTransactionWithCompressionInfo(sig.signature);
return {
signature: sig.signature,
slot: sig.slot,
blockTime: sig.blockTime,
timestamp: new Date(sig.blockTime * 1000).toISOString(),
compressionInfo: txInfo?.compressionInfo
? {
closedAccounts: txInfo.compressionInfo.closedAccounts.length,
openedAccounts: txInfo.compressionInfo.openedAccounts.length,
}
: null,
};
})
);
setTransactions(transactionsWithDetails);
return;
}
const basicTransactions = limitedSignatures.map((sig) => ({
signature: sig.signature,
slot: sig.slot,
blockTime: sig.blockTime,
timestamp: new Date(sig.blockTime * 1000).toISOString(),
}));
setTransactions(basicTransactions);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
setError(message);
setTransactions([]);
} finally {
setIsLoading(false);
}
},
[]
);
return { transactions, isLoading, error, fetchTransactionHistory };
}
- Node.js
- React
Report incorrect code
Copy
Ask AI
---
argument-hint: <project_path>
description: Add compressed token support to Privy Node.js app
allowed-tools: [Bash, Read, Glob, Grep, Task, mcp__deepwiki, mcp__zkcompression]
---
## Task: Add compressed token support to Privy Node.js app
References:
- Privy Docs: https://docs.privy.io/recipes/solana/send-spl-tokens
- Privy Node Examples:
- SPL: https://github.com/privy-io/examples/tree/main/privy-node-starter
- Compressed: https://github.com/Lightprotocol/examples-zk-compression/tree/main/privy/nodejs-privy-compressed
- SPL vs Compressed comparison: https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/COMPARISON.md
- Compressed Token Guide for Privy Node.js: https://zkcompression.com/compressed-tokens/for-privy
MCP:
* deepwiki https://mcp.deepwiki.com/mcp
* ZK Compression https://www.zkcompression.com/mcp
## Workflow
- This plan must execute without user intervention
- All questions have been resolved in planning phase
- If blocked, find alternative approach - do not stop
- Keep working until ALL todos are complete
- Use Task tool with subagents for parallel research or when stuck
- Use subagents with Read, Glob, Grep, and Deepwiki permissions when stuck
- Always assign Tasks to subagents and tell the user
## DeepWiki fallback
```
mcp__deepwiki__read_wiki_structure("Lightprotocol/light-protocol"),
mcp__deepwiki__read_wiki_contents("Lightprotocol/light-protocol"),
mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "<question>")
```
## Quick Reference
| Operation | SPL | Compressed |
| -------------- | ------------------------------ | ---------------------------------------------- |
| Transfer | transferChecked() | transfer() |
| Compress SPL to recipient | N/A | compress() |
| Decompress to SPL | N/A | decompress() |
| Get Balance | getAccount() | getCompressedTokenAccountsByOwner() |
| Tx History | getSignaturesForAddress() | getCompressionSignaturesForOwner() |
### Phase 1: Index project
```bash
grep -r "createTransferInstruction\|getAssociatedTokenAddress\|@solana/spl-token" src/
```
### Phase 2: Add dependencies
```bash
npm install @lightprotocol/stateless.js @lightprotocol/compressed-token
```
### Phase 3: Implement operations
See Code Reference below all signed with Privy SDK.
1. **Setup**: Connect to ZK Compression RPC (Helius, Triton) https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/nodejs-privy-compressed/src/index.ts
2. **Get Balance**: Fetch SPL and compressed token balances https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/nodejs-privy-compressed/src/compressed/balances.ts
3. **Transfer**: Send compressed tokens to another recipient, signed with Privy SDK https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/nodejs-privy-compressed/src/compressed/transfer.ts
4. **Compress**: Convert SPL to compressed tokens and send to a recipient in one instruction https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/nodejs-privy-compressed/src/compressed/compress.ts
5. **Decompress**: Convert compressed tokens back to SPL tokens for offramps https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/nodejs-privy-compressed/src/compressed/decompress.ts
6. **Tx History**: Fetch compressed token transaction history for an owner
### Phase 4: Test
Run the implemented functions. On failure, debug and retry. Assign always to subagents with Tasktool
Report incorrect code
Copy
Ask AI
---
argument-hint: <project_path>
description: Add compressed token support to Privy React app
allowed-tools: [Bash, Read, Glob, Grep, Task, mcp__deepwiki, mcp__zkcompression]
---
## Task: Add compressed token support to Privy React app
References:
- Privy Docs: https://docs.privy.io/recipes/solana/send-spl-tokens
- Privy React Examples:
- SPL: https://github.com/privy-io/examples/tree/main/privy-react-starter
- Compressed: https://github.com/Lightprotocol/examples-zk-compression/tree/main/privy/react-privy-compressed
- SPL vs Compressed comparison: https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/COMPARISON.md
- Compressed Token Guide for Privy React: https://zkcompression.com/compressed-tokens/for-privy
MCP:
* deepwiki https://mcp.deepwiki.com/mcp
* ZK Compression https://www.zkcompression.com/mcp
## Workflow
- This plan must execute without user intervention
- All questions have been resolved in planning phase
- If blocked, find alternative approach - do not stop
- Keep working until ALL todos are complete
- Use Task tool with subagents for parallel research or when stuck
- Use subagents with Read, Glob, Grep, and Deepwiki permissions when stuck
- Always assign Tasks to subagents and tell the user
## DeepWiki fallback
```
mcp__deepwiki__read_wiki_structure("Lightprotocol/light-protocol"),
mcp__deepwiki__read_wiki_contents("Lightprotocol/light-protocol"),
mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "<question>")
```
## Quick Reference
| Operation | SPL | Compressed |
| -------------- | ------------------------------ | ---------------------------------------------- |
| Transfer | transferChecked() | transfer() |
| Compress SPL to recipient | N/A | compress() |
| Decompress to SPL | N/A | decompress() |
| Get Balance | getAccount() | getCompressedTokenAccountsByOwner() |
| Tx History | getSignaturesForAddress() | getCompressionSignaturesForOwner() |
### Phase 1: Index project
```bash
grep -r "createTransferInstruction\|getAssociatedTokenAddress\|@solana/spl-token" src/
```
### Phase 2: Add dependencies
```bash
npm install @lightprotocol/stateless.js @lightprotocol/compressed-token
```
### Phase 3: Implement operations
See Code Reference below all signed with Privy SDK.
1. **Setup**: Connect to ZK Compression RPC (Helius, Triton) https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/react-privy-compressed/src/config/rpc.ts
2. **Get Balance**: Fetch SPL and compressed token balances https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/react-privy-compressed/src/hooks/useCompressedBalances.ts
3. **Transfer**: Send compressed tokens to another recipient, signed with Privy SDK https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/react-privy-compressed/src/hooks/useTransfer.ts
4. **Compress**: Convert SPL to compressed tokens and send to a recipient in one instruction https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/react-privy-compressed/src/hooks/useCompress.ts
5. **Decompress**: Convert compressed tokens back to SPL tokens for offramps https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/react-privy-compressed/src/hooks/useDecompress.ts
6. **Tx History**: Fetch compressed token transaction history for an owner
### Phase 4: Test
Run the implemented functions. On failure, debug and retry. Assign always to subagents with Tasktool