| 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 | |
|---|---|---|
| Get Balance | getAccount() | getCompressedTokenAccountsByOwner() |
| Transfer | transferChecked() | transfer() |
| Compress | N/A | compress() |
| Decompress | N/A | decompress() |
| Transaction History | getSignaturesForAddress() | getCompressionSignaturesForOwner() |
1
Prerequisites
Report incorrect code
Copy
Ask AI
npm install @lightprotocol/compressed-token@alpha \
@lightprotocol/stateless.js@alpha
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 can compress or decompresss, we need:
- An SPL mint with a token pool for compression. This token pool 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
Find a complete example on Github: privy/nodejs-privy-compressed.
- 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
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;```
Convert SPL to compressed tokens and send to a recipient in one instruction.
- Node.js
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 compressSplTokens = 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 compressSplTokens;
Convert compressed tokens back to SPL tokens.
- Node.js
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;
Get balances
Fetch SPL and compressed token balances.- Node.js
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 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 {
tokens: Array.from(balances.entries()).map(([mint, amount]) => ({
mint,
amount: amount.toString(),
accounts: validItems.filter(a => a.parsed?.mint.toBase58() === mint).length
}))
};
}
Get transaction history
Fetch compressed token transaction history for an owner, with optional detailed compression information.- Node.js
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;