- Client Side Distribution
- Claim Reference Implementations
Choose your implementation based on your needs:
Add decompression of SPL Tokens with this script.
| Tab | Best For | What You’ll Get | Time |
|---|---|---|---|
| Localnet Guide | First-time users, learning | Step-by-step Localnet tutorial | 20 min |
| Simple Airdrop | <10,000 recipients | Production-ready single script | 10 min |
| Airdrop with Batched Instructions | 10,000+ recipients | Batched system with retry logic | 15 min |
Via Webapp you can use Airship by Helius Labs to airdrop to up to 200,000 recipients.
- Localnet Guide
- Simple Airdrop
- Airdrop with Batched Instructions
What you’ll build: A test airdrop sending compressed tokens to 3 recipients on your local validator.
1
Prerequisites
Make sure you have dependencies and developer environment set up!System Requirements
- Node.js >= 20.18.0 (required by latest Solana packages)
- npm or yarn package manager
Prerequisites & Setup
Prerequisites & Setup
DependenciesDeveloper Environment
- npm
- yarn
- pnpm
Report incorrect code
Copy
Ask AI
npm install @lightprotocol/stateless.js@alpha \
@lightprotocol/compressed-token@alpha
Report incorrect code
Copy
Ask AI
yarn add @lightprotocol/stateless.js@alpha \
@lightprotocol/compressed-token@alpha
Report incorrect code
Copy
Ask AI
pnpm add @lightprotocol/stateless.js@alpha \
@lightprotocol/compressed-token@alpha
- Localnet
- Devnet
By default, all guides use Localnet.
- npm
- yarn
- pnpm
Report incorrect code
Copy
Ask AI
npm install -g @lightprotocol/zk-compression-cli@alpha
Report incorrect code
Copy
Ask AI
yarn global add @lightprotocol/zk-compression-cli@alpha
Report incorrect code
Copy
Ask AI
pnpm add -g @lightprotocol/zk-compression-cli@alpha
Report incorrect code
Copy
Ask AI
# Start a local test validator
light test-validator
## ensure you have the Solana CLI accessible in your system PATH
Report incorrect code
Copy
Ask AI
// 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();
Replace
<your-api-key> with your actual API key. Get your API key here, if you don’t have one yet.Report incorrect code
Copy
Ask AI
import { createRpc } from "@lightprotocol/stateless.js";
const RPC_ENDPOINT = "https://devnet.helius-rpc.com?api-key=<your_api_key>";
const connection = createRpc(RPC_ENDPOINT);
async function main() {
let slot = await connection.getSlot();
console.log(slot);
let health = await connection.getIndexerHealth(slot);
console.log(health);
// "Ok"
}
main();
2
Mint SPL tokens to your wallet
Run thismint-spl-tokens.ts to mint SPL tokens to your wallet.mint-spl-tokens.ts
Report incorrect code
Copy
Ask AI
// Mint SPL Tokens for Airdrop - LocalNet
// 1. Load wallet and connect to local validator
// 2. Create SPL mint with token pool for compression via createMint()
// 3. Create ATA and mint SPL tokens to sender for airdrop preparation
// 4. Output mint address for use in simple-airdrop.ts
import { Keypair } from "@solana/web3.js";
import { createRpc } from "@lightprotocol/stateless.js";
import {
createMint,
getOrCreateAssociatedTokenAccount,
mintTo,
} from "@solana/spl-token";
import { createTokenPool } from "@lightprotocol/compressed-token";
import * as fs from 'fs';
import * as os from 'os';
// Step 1: Setup local connection and load wallet
const connection = 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));
(async () => {
// Step 2: Create SPL mint with token pool for compression
const mint = await createMint(connection, payer, payer.publicKey, null, 9);
const poolTxId = await createTokenPool(connection, payer, mint);
console.log(`Mint address: ${mint.toBase58()}`);
console.log(`TokenPool created: ${poolTxId}`);
// Step 3: Create associated token account for sender
// The sender will send tokens from this account to the recipients as compressed tokens.
const ata = await getOrCreateAssociatedTokenAccount(
connection,
payer,
mint, // SPL mint with token pool for compression
payer.publicKey
);
console.log(`ATA address: ${ata.address.toBase58()}`);
// Step 4: Mint SPL tokens to ATA.
// The sender will send tokens from this account to the recipients as compressed tokens.
const mintToTxId = await mintTo(
connection,
payer,
mint, // SPL mint with token pool for compression
ata.address, // distributor ATA
payer.publicKey,
100_000_000_000 // amount: 100 tokens with 9 decimals
);
console.log(`\nSPL tokens minted and ready for distribution!`);
console.log(`Transaction: ${mintToTxId}`);
console.log(`\nCopy mint address to your airdrop script: ${mint.toBase58()}`);
})();
3
Execute the Airdrop
Next, distribute the SPL tokens to all recipients.Ensure you have the latest
@lightprotocol/stateless.js and @lightprotocol/compressed-token versions ≥ 0.21.0!simple-airdrop.ts
Report incorrect code
Copy
Ask AI
// Simple Airdrop - LocalNet
// 1. Load wallet and select compression infrastructure with getStateTreeInfos() and getTokenPoolInfos()
// 2. Build CompressedTokenProgram.compress() instruction for multiple recipients in one transaction
// 3. Execute transaction with compute budget and confirm compression operation with sendAndConfirmTx()
// 4. Verify distribution via getCompressedTokenAccountsByOwner
import { Keypair, PublicKey, ComputeBudgetProgram } from "@solana/web3.js";
import {
CompressedTokenProgram,
getTokenPoolInfos,
selectTokenPoolInfo,
} from "@lightprotocol/compressed-token";
import {
bn,
buildAndSignTx,
calculateComputeUnitPrice,
createRpc,
dedupeSigner,
Rpc,
selectStateTreeInfo,
sendAndConfirmTx,
} from "@lightprotocol/stateless.js";
import { getOrCreateAssociatedTokenAccount } from "@solana/spl-token";
import * as fs from 'fs';
import * as os from 'os';
// Step 1: Setup local connection and load wallet
const connection: Rpc = createRpc(); // defaults to localhost:8899
const mint = new PublicKey("MINTADDRESS"); // Replace with mint address from mint-spl-tokens.ts
// Local uses file wallet. Use constants from .env file in production
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;
(async () => {
// Step 2: Select state tree and token pool
const activeStateTrees = await connection.getStateTreeInfos();
const treeInfo = selectStateTreeInfo(activeStateTrees);
const infos = await getTokenPoolInfos(connection, mint);
const info = selectTokenPoolInfo(infos);
// Step 3: Get or create source token account for distribution
// The sender will send tokens from this account to the recipients as compressed tokens.
const sourceTokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
payer,
mint, // SPL mint with token pool for compression
payer.publicKey
);
// Step 4: Define airdrop recipients and amounts
const airDropAddresses = [
Keypair.generate().publicKey,
Keypair.generate().publicKey,
Keypair.generate().publicKey,
];
const amounts = [
bn(20_000_000_000), // 20 tokens
bn(30_000_000_000), // 30 tokens
bn(40_000_000_000), // 40 tokens
];
const totalAmount = amounts.reduce((sum, amt) => sum + amt.toNumber(), 0);
console.log(`Distributing ${totalAmount / 1e9} compressed tokens to ${airDropAddresses.length} recipients`);
const initialSplBalance = await connection.getTokenAccountBalance(sourceTokenAccount.address);
console.log(`Sender initial balance: ${initialSplBalance.value.uiAmount} tokens`);
// Step 5: Build transaction with compute budget and compression instruction
const instructions = [];
// Set compute unit limits based on recipient count (estimated 120k CU per recipient)
instructions.push(
ComputeBudgetProgram.setComputeUnitLimit({ units: 120_000 * airDropAddresses.length }),
ComputeBudgetProgram.setComputeUnitPrice({
microLamports: calculateComputeUnitPrice(20_000, 120_000 * airDropAddresses.length), // dynamic priority fee
})
);
// Create compression instruction for multiple recipients in one transaction
const compressInstruction = await CompressedTokenProgram.compress({
payer: payer.publicKey,
owner: owner.publicKey,
source: sourceTokenAccount.address, // source ATA holding SPL tokens
toAddress: airDropAddresses, // recipient addresses for compressed tokens
amount: amounts, // different amounts for each recipient
mint, // SPL mint with token pool for compression
tokenPoolInfo: info,
outputStateTreeInfo: treeInfo, // destination state tree
});
instructions.push(compressInstruction);
// Step 6: Sign and send transaction
const additionalSigners = dedupeSigner(payer, [owner]);
const { blockhash } = await connection.getLatestBlockhash();
const tx = buildAndSignTx(instructions, payer, blockhash, additionalSigners);
// For production: Add address lookup table to reduce transaction size and fees
// const lookupTableAddress = new PublicKey("9NYFyEqPkyXUhkerbGHXUXkvb4qpzeEdHuGpgbgpH1NJ"); // mainnet // or "qAJZMgnQJ8G6vA3WRcjD9Jan1wtKkaCFWLWskxJrR5V" for devnet
// const lookupTableAccount = (await connection.getAddressLookupTable(lookupTableAddress)).value!;
// const tx = buildAndSignTx(instructions, payer, blockhash, additionalSigners, [lookupTableAccount]);
const txId = await sendAndConfirmTx(connection, tx);
console.log(`\nAirdrop completed!`);
console.log(`Transaction: ${txId}`);
// Step 7: Verify distribution via getCompressedTokenAccountsByOwner
for (let i = 0; i < airDropAddresses.length; i++) {
const recipientAccounts = await connection.getCompressedTokenAccountsByOwner(airDropAddresses[i], { mint });
const balance = recipientAccounts.items.reduce((sum, account) => sum + Number(account.parsed.amount), 0);
console.log(`Recipient ${i + 1} (${airDropAddresses[i].toString()}): ${balance / 1e9} compressed tokens`);
}
const finalSplBalance = await connection.getTokenAccountBalance(sourceTokenAccount.address);
console.log(`\nSender balance after airdrop: ${finalSplBalance.value.uiAmount} SPL tokens`);
return txId;
})();
For small airdrops (<10,000 recipients). View the source code here.
1
Environment Setup
Configure your environment variables:.env
Report incorrect code
Copy
Ask AI
PAYER_KEYPAIR=YOUR_BASE58_ENCODED_PRIVATE_KEY
MINT_ADDRESS=YOUR_MINT_ADDRESS
2
Mint SPL Tokens
Use your existing mint or mint SPL tokens to your wallet for distribution.Report incorrect code
Copy
Ask AI
import { Rpc, createRpc } from '@lightprotocol/stateless.js';
import { createMint } from '@lightprotocol/compressed-token';
import {
getOrCreateAssociatedTokenAccount,
mintTo as mintToSpl,
} from '@solana/spl-token';
import { PAYER_KEYPAIR, RPC_ENDPOINT } from '../constants';
const payer = PAYER_KEYPAIR;
const connection: Rpc = createRpc(RPC_ENDPOINT);
const decimals = 9;
const mintAmount = 100;
(async () => {
// airdrop lamports to pay tx fees
// await confirmTx(
// connection,
// await connection.requestAirdrop(payer.publicKey, 1e7)
// );
const { mint, transactionSignature } = await createMint(
connection,
payer,
payer.publicKey,
decimals,
);
console.log(
`create-mint success! txId: ${transactionSignature}, mint: ${mint.toBase58()}`,
);
const ata = await getOrCreateAssociatedTokenAccount(
connection,
payer,
mint,
payer.publicKey,
);
console.log(`ata: ${ata.address}`);
const mintTxId = await mintToSpl(
connection,
payer,
mint,
ata.address,
payer.publicKey,
mintAmount,
);
console.log(`mint-spl success! txId: ${mintTxId}`);
})();
3
Execute Airdrop
Run the airdrop script with your configured environment:Report incorrect code
Copy
Ask AI
// 1. Load environment and select compression infrastructure with getStateTreeInfos() and getTokenPoolInfos()
// 2. Build CompressedTokenProgram.compress() instruction for multiple recipients in one transaction
// 3. Execute transaction with compute budget, address lookup table, and confirm with sendAndConfirmTx()
// 4. Verify distribution via getCompressedTokenAccountsByOwner
import {
PublicKey,
TransactionInstruction,
ComputeBudgetProgram,
} from '@solana/web3.js';
import {
CompressedTokenProgram,
getTokenPoolInfos,
selectTokenPoolInfo,
} from '@lightprotocol/compressed-token';
import {
bn,
buildAndSignTx,
calculateComputeUnitPrice,
createRpc,
dedupeSigner,
Rpc,
selectStateTreeInfo,
sendAndConfirmTx,
} from '@lightprotocol/stateless.js';
import { getOrCreateAssociatedTokenAccount } from '@solana/spl-token';
import { MINT_ADDRESS, PAYER_KEYPAIR, RPC_ENDPOINT } from '../constants';
(async () => {
const connection: Rpc = createRpc(RPC_ENDPOINT);
const mintAddress = MINT_ADDRESS;
const payer = PAYER_KEYPAIR;
const owner = payer;
const recipients = [
PublicKey.default,
// ...
];
// 1. Select a state tree
const treeInfos = await connection.getStateTreeInfos(); // Fixed: removed deprecated getCachedActiveStateTreeInfos
const treeInfo = selectStateTreeInfo(treeInfos);
// 2. Select a token pool
const tokenPoolInfos = await getTokenPoolInfos(connection, mintAddress);
const tokenPoolInfo = selectTokenPoolInfo(tokenPoolInfos);
// Create an SPL token account for the sender.
// The sender will send tokens from this account to the recipients as compressed tokens.
const sourceTokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
payer,
mintAddress,
payer.publicKey,
);
// 1 recipient = 120_000 CU
// 5 recipients = 170_000 CU
const units = 120_000;
const amount = bn(333);
// To land faster, replace this with a dynamic fee based on network
// conditions.
const microLamports = calculateComputeUnitPrice(20_000, units);
const instructions: TransactionInstruction[] = [
ComputeBudgetProgram.setComputeUnitLimit({ units }),
ComputeBudgetProgram.setComputeUnitPrice({
microLamports,
}),
];
const compressInstruction = await CompressedTokenProgram.compress({
payer: payer.publicKey,
owner: owner.publicKey,
source: sourceTokenAccount.address,
toAddress: recipients,
amount: recipients.map(() => amount),
mint: mintAddress,
outputStateTreeInfo: treeInfo,
tokenPoolInfo,
});
instructions.push(compressInstruction);
// https://www.zkcompression.com/developers/protocol-addresses-and-urls#lookup-tables
const lookupTableAddress = new PublicKey(
'9NYFyEqPkyXUhkerbGHXUXkvb4qpzeEdHuGpgbgpH1NJ', // mainnet
// "qAJZMgnQJ8G6vA3WRcjD9Jan1wtKkaCFWLWskxJrR5V" // devnet
);
// Get the lookup table account state
const lookupTableAccount = (
await connection.getAddressLookupTable(lookupTableAddress)
).value!;
const additionalSigners = dedupeSigner(payer, [owner]);
const { blockhash } = await connection.getLatestBlockhash();
const tx = buildAndSignTx(
instructions,
payer,
blockhash,
additionalSigners,
[lookupTableAccount],
);
const txId = await sendAndConfirmTx(connection, tx);
console.log(`txId: ${txId}`);
})();
For large-scale airdrops (10,000+ recipients) we recommend batched instructions.
View the source code here.
View the source code here.
This implementation creates the mint and executes the airdrop in a single operation. You only need to configure your RPC endpoint and payer keypair.
1
Create Instructions
Process recipients in chunks and create batched instructions with optimized compute limits.create-instructions.ts
Report incorrect code
Copy
Ask AI
// 1. Process recipients in chunks with selectStateTreeInfo() and selectTokenPoolInfo() for each batch
// 2. Create CompressedTokenProgram.compress() instructions with ComputeBudgetProgram limits for multiple recipients
// 3. Return batched instructions for optimized large-scale airdrop execution
import {
CompressedTokenProgram,
TokenPoolInfo,
} from "@lightprotocol/compressed-token";
import {
bn,
selectStateTreeInfo,
StateTreeInfo,
} from "@lightprotocol/stateless.js";
import {
ComputeBudgetProgram,
TransactionInstruction,
PublicKey,
} from "@solana/web3.js";
interface CreateAirdropInstructionsParams {
amount: number | bigint;
recipients: PublicKey[];
payer: PublicKey;
sourceTokenAccount: PublicKey;
mint: PublicKey;
stateTreeInfos: StateTreeInfo[];
tokenPoolInfos: TokenPoolInfo[];
maxRecipientsPerInstruction?: number;
maxInstructionsPerTransaction?: number;
computeUnitLimit?: number;
computeUnitPrice?: number | undefined;
}
export type InstructionBatch = TransactionInstruction[];
export async function createAirdropInstructions({
amount,
recipients,
payer,
sourceTokenAccount,
mint,
stateTreeInfos,
tokenPoolInfos,
maxRecipientsPerInstruction = 5,
maxInstructionsPerTransaction = 3,
computeUnitLimit = 500_000,
computeUnitPrice = undefined,
}: CreateAirdropInstructionsParams): Promise<InstructionBatch[]> {
const instructionBatches: InstructionBatch[] = [];
const amountBn = bn(amount.toString());
// Process recipients in chunks
for (
let i = 0;
i < recipients.length;
i += maxRecipientsPerInstruction * maxInstructionsPerTransaction
) {
const instructions: TransactionInstruction[] = [];
instructions.push(
ComputeBudgetProgram.setComputeUnitLimit({ units: computeUnitLimit })
);
if (computeUnitPrice) {
instructions.push(
ComputeBudgetProgram.setComputeUnitPrice({
microLamports: computeUnitPrice,
})
);
}
const treeInfo = selectStateTreeInfo(stateTreeInfos);
const tokenPoolInfo = selectTokenPoolInfo(tokenPoolInfos);
for (let j = 0; j < maxInstructionsPerTransaction; j++) {
const startIdx = i + j * maxRecipientsPerInstruction;
const recipientBatch = recipients.slice(
startIdx,
startIdx + maxRecipientsPerInstruction
);
if (recipientBatch.length === 0) break;
const compressIx = await CompressedTokenProgram.compress({
payer,
owner: payer,
source: sourceTokenAccount,
toAddress: recipientBatch,
amount: recipientBatch.map(() => amountBn),
mint,
tokenPoolInfo,
outputStateTreeInfo: treeInfo,
});
instructions.push(compressIx);
}
if (
(computeUnitPrice && instructions.length > 2) ||
(!computeUnitPrice && instructions.length > 1)
) {
instructionBatches.push(instructions);
}
}
return instructionBatches;
}
2
Update Blockhash
Maintain fresh blockhashes with background refresh loop usinggetLatestBlockhash() every 30 seconds.update-blockhash.ts
Report incorrect code
Copy
Ask AI
import { Rpc } from "@lightprotocol/stateless.js";
// 1. Fetch initial blockhash with getLatestBlockhash() and store in exported variable
// 2. Set up background refresh loop with setTimeout() to update blockhash every 30 seconds
// 3. Provide AbortSignal support to stop background updates when airdrop completes
export let currentBlockhash: string;
export async function updateBlockhash(
connection: Rpc,
signal: AbortSignal
): Promise<void> {
try {
const { blockhash } = await connection.getLatestBlockhash();
currentBlockhash = blockhash;
console.log(`Initial blockhash: ${currentBlockhash}`);
} catch (error) {
console.error("Failed to fetch initial blockhash:", error);
return;
}
// Update blockhash in the background
(function updateInBackground() {
if (signal.aborted) return;
const timeoutId = setTimeout(async () => {
if (signal.aborted) return;
try {
const { blockhash } = await connection.getLatestBlockhash();
currentBlockhash = blockhash;
console.log(`Updated blockhash: ${currentBlockhash}`);
} catch (error) {
console.error("Failed to update blockhash:", error);
}
updateInBackground();
}, 30_000);
signal.addEventListener("abort", () => clearTimeout(timeoutId));
})();
}
3
Sign and Send
Execute batched transactions withVersionedTransaction, retry logic, and sendAndConfirmTx() confirmation.sign-and-send.ts
Report incorrect code
Copy
Ask AI
// 1. Initialize blockhash updates with updateBlockhash() and get address lookup table with getAddressLookupTable()
// 2. Process instruction batches with VersionedTransaction and retry logic for failed transactions
// 3. Yield batch results with sendAndConfirmTx() confirmation and comprehensive error handling
import { Rpc, sendAndConfirmTx } from "@lightprotocol/stateless.js";
import {
Keypair,
PublicKey,
TransactionMessage,
VersionedTransaction,
} from "@solana/web3.js";
import { InstructionBatch } from "./create-instructions";
import { currentBlockhash, updateBlockhash } from "./update-blockhash";
import bs58 from "bs58";
export enum BatchResultType {
Success = "success",
Error = "error",
}
export type BatchResult =
| { type: BatchResultType.Success; index: number; signature: string }
| { type: BatchResultType.Error; index: number; error: string };
export async function* signAndSendAirdropBatches(
batches: InstructionBatch[],
payer: Keypair,
connection: Rpc,
maxRetries = 3
): AsyncGenerator<BatchResult> {
const abortController = new AbortController();
const { signal } = abortController;
await updateBlockhash(connection, signal);
const statusMap = new Array(batches.length).fill(0); // Initialize all as pending (0)
// Use zk-compression look up table for your network
// https://www.zkcompression.com/developers/protocol-addresses-and-urls#lookup-tables
const lookupTableAddress = new PublicKey(
"9NYFyEqPkyXUhkerbGHXUXkvb4qpzeEdHuGpgbgpH1NJ"
);
// Get the lookup table account
const lookupTableAccount = (
await connection.getAddressLookupTable(lookupTableAddress)
).value!;
while (statusMap.includes(0)) {
// Continue until all are confirmed or errored
const pendingBatches = statusMap.filter((status) => status === 0).length;
console.log(`Sending ${pendingBatches} transactions`);
const sends = statusMap.map(async (status, index) => {
if (status !== 0) return; // Skip non-pending batches
let retries = 0;
while (retries < maxRetries && statusMap[index] === 0) {
if (!currentBlockhash) {
console.warn("Waiting for blockhash to be set...");
await new Promise((resolve) => setTimeout(resolve, 1000));
continue;
}
try {
const tx = new VersionedTransaction(
new TransactionMessage({
payerKey: payer.publicKey,
recentBlockhash: currentBlockhash,
instructions: batches[index],
}).compileToV0Message([lookupTableAccount])
);
tx.sign([payer]);
const sig = bs58.encode(tx.signatures[0]);
console.log(`Batch ${index} signature: ${sig}`);
const confirmedSig = await sendAndConfirmTx(connection, tx, {
skipPreflight: true,
commitment: "confirmed",
});
if (confirmedSig) {
statusMap[index] = 1; // Mark as confirmed
return {
type: BatchResultType.Success,
index,
signature: confirmedSig,
};
}
} catch (e) {
retries++;
console.warn(`Retrying batch ${index}, attempt ${retries + 1}`);
if (retries >= maxRetries) {
statusMap[index] = `err: ${(e as Error).message}`; // Mark as error
return {
type: BatchResultType.Error,
index,
error: (e as Error).message,
};
}
}
}
});
const results = await Promise.all(sends);
for (const result of results) {
if (result) yield result as BatchResult;
}
}
// Stop the blockhash update loop
abortController.abort();
}
4
Main Airdrop File
Put it all together in the main airdrop file.airdrop.ts
Report incorrect code
Copy
Ask AI
// 1. Create compressed mint with createMint(), mint supply with mintTo(), get infrastructure with getStateTreeInfos() and getTokenPoolInfos()
// 2. Generate batched compression instructions with createAirdropInstructions() - create CompressedTokenProgram.compress() calls
// 3. Execute batched airdrop with signAndSendAirdropBatches() - sign transactions and confirm with sendAndConfirmTx() for large-scale distribution
import { Keypair, LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js";
import {
calculateComputeUnitPrice,
createRpc,
Rpc,
} from "@lightprotocol/stateless.js";
import { createMint, getTokenPoolInfos } from "@lightprotocol/compressed-token";
import { getOrCreateAssociatedTokenAccount, mintTo } from "@solana/spl-token";
import { createAirdropInstructions } from "./create-instructions";
import { BatchResultType, signAndSendAirdropBatches } from "./sign-and-send";
import dotenv from "dotenv";
import bs58 from "bs58";
dotenv.config();
// Step 1: Setup environment and RPC connection
const RPC_ENDPOINT = `https://mainnet.helius-rpc.com?api-key=${process.env.HELIUS_API_KEY}`;
const connection: Rpc = createRpc(RPC_ENDPOINT);
const PAYER = Keypair.fromSecretKey(bs58.decode(process.env.PAYER_KEYPAIR!));
// Step 2: Define airdrop recipient list (20 example addresses)
const recipients = [
"GMPWaPPrCeZPse5kwSR3WUrqYAPrVZBSVwymqh7auNW7",
"GySGrTgPtPfMtYoYTmwUdUDFwVJbFMfip7QZdhgXp8dy",
"Bk1r2vcgX2uTzwV3AUyfRbSfGKktoQrQufBSrHzere74",
"8BvkadZ6ycFNmQF7S1MHRvEVNb1wvDBFdjkAUnxjK9Ug",
"EmxcvFKXsWLzUho8AhV9LCKeKRFHg5gAs4sKNJwhe5PF",
"6mqdHkSpcvNexmECjp5XLt9V9KnSQre9TvbMLGr6sEPM",
"3k4MViTWXBjFvoUZiJcNGPvzrqnTa41gcrbWCMMnV6ys",
"2k6BfYRUZQHquPtpkyJpUx3DzM7W3K6H95igtJk8ztpd",
"89jPyNNLCcqWn1RZThSS4jSqU5VCJkR5mAaSaVzuuqH4",
"3MzSRLf9jSt6d1MFFMMtPfUcDY6XziRxTB8C5mfvgxXG",
"9A1H6f3N8mpAPSdfqvYRD4cM1NwDZoMe3yF5DwibL2R2",
"PtUAhLvUsVcoesDacw198SsnMoFNVskR5pT3QvsBSQw",
"6C6W6WpgFK8TzTTMNCPMz2t9RaMs4XnkfB6jotrWWzYJ",
"8sLy9Jy8WSh6boq9xgDeBaTznn1wb1uFpyXphG3oNjL5",
"GTsQu2XCgkUczigdBFTWKrdDgNKLs885jKguyhkqdPgV",
"85UK4bjC71Jwpyn8mPSaW3oYyEAiHPbESByq9s5wLcke",
"9aEJT4CYHEUWwwSQwueZc9EUjhWSLD6AAbpVmmKDeP7H",
"CY8QjRio1zd9bYWMKiVRrDbwVenf3JzsGf5km5zLgY9n",
"CeHbdxgYifYhpB6sXGonKzmaejqEfq2ym5utTmB6XMVv",
"4z1qss12DjUzGUkK1fFesqrUwrEVJJvzPMNkwqYnbAR5",
].map((address) => new PublicKey(address));
(async () => {
// Step 3: Create compressed mint and register for compression
// 3a: Call createMint() to initialize mint with compression pool
const { mint, transactionSignature } = await createMint(
connection,
PAYER, // fee payer
PAYER.publicKey, // mint authority
9 // decimals
);
console.log(
`create-mint success! txId: ${transactionSignature}, mint: ${mint.toBase58()}`
);
// Step 4: Create associated token account for distributor
// 4a: Ensure PAYER has ATA for holding tokens before compression
const ata = await getOrCreateAssociatedTokenAccount(
connection,
PAYER, // fee payer
mint, // token mint
PAYER.publicKey // token owner
);
console.log(`ATA: ${ata.address.toBase58()}`);
// Step 5: Mint initial token supply to distributor
// 5a: Create 10 billion tokens in the ATA for airdrop distribution
const mintToTxId = await mintTo(
connection,
PAYER, // fee payer and mint authority
mint, // token mint
ata.address, // destination ATA
PAYER.publicKey, // mint authority
10e9 * LAMPORTS_PER_SOL // amount: 10 billion tokens with decimals
);
console.log(`mint-to success! txId: ${mintToTxId}`);
// Step 6: Get compression infrastructure for batch operations
// 6a: Fetch available state trees for compressed account storage
const stateTreeInfos = await connection.getStateTreeInfos();
// 6b: Get token pool infos for compression operations
const tokenPoolInfos = await getTokenPoolInfos(connection, mint);
// Step 7: Create instruction batches for large-scale airdrop
// 7a: Generate batched compression instructions with compute optimization
const instructionBatches = await createAirdropInstructions({
amount: 1e6, // 1 million tokens per recipient
recipients, // array of recipient addresses
payer: PAYER.publicKey, // transaction fee payer
sourceTokenAccount: ata.address, // source ATA holding SPL tokens
mint, // token mint
stateTreeInfos, // state trees for compressed accounts
tokenPoolInfos, // token pools for compression
computeUnitPrice: calculateComputeUnitPrice(10_000, 500_000), // dynamic priority fee
});
// Step 8: Execute batched airdrop with error handling
// 8a: Process instruction batches with retry logic and confirmation
for await (const result of signAndSendAirdropBatches(
instructionBatches,
PAYER,
connection
)) {
if (result.type === BatchResultType.Success) {
console.log(`Batch ${result.index} confirmed: ${result.signature}`);
} else if (result.type === BatchResultType.Error) {
console.log(`Batch ${result.index} failed: ${result.error}`);
// Use result.index to access the specific batch in instructionBatches
const failedBatch = instructionBatches[result.index];
console.log(`Failed batch instructions:`, failedBatch);
// Additional logic to handle failed instructions
}
}
console.log("Airdrop process complete.");
})();
Advanced Features
Solana Wallets like Phantom and Backpack already support compressed tokens.
Still, you can let users decompress to SPL via your Frontend to customize claims.
Report incorrect code
Copy
Ask AI
import {
bn,
buildAndSignTx,
sendAndConfirmTx,
dedupeSigner,
Rpc,
createRpc,
} from "@lightprotocol/stateless.js";
import { ComputeBudgetProgram, Keypair, PublicKey } from "@solana/web3.js";
import {
CompressedTokenProgram,
getTokenPoolInfos,
selectMinCompressedTokenAccountsForTransfer,
selectTokenPoolInfosForDecompression,
} from "@lightprotocol/compressed-token";
import { getOrCreateAssociatedTokenAccount } from "@solana/spl-token";
import bs58 from "bs58";
import dotenv from "dotenv";
dotenv.config();
// Set these values in your .env file
const RPC_ENDPOINT = process.env.RPC_ENDPOINT;
const mint = new PublicKey(process.env.MINT_ADDRESS!);
const payer = Keypair.fromSecretKey(bs58.decode(process.env.PAYER_KEYPAIR!));
const owner = payer;
const amount = 1e5;
const connection: Rpc = createRpc(RPC_ENDPOINT);
(async () => {
// 1. Create an associated token account for the user if it doesn't exist
const ata = await getOrCreateAssociatedTokenAccount(
connection,
payer,
mint,
payer.publicKey
);
// 2. Fetch compressed token accounts
const compressedTokenAccounts =
await connection.getCompressedTokenAccountsByOwner(owner.publicKey, {
mint,
});
// 3. Select
const [inputAccounts] = selectMinCompressedTokenAccountsForTransfer(
compressedTokenAccounts.items,
bn(amount)
);
// 4. Fetch validity proof
const proof = await connection.getValidityProof(
inputAccounts.map((account) => account.compressedAccount.hash)
);
// 5. Fetch token pool infos
const tokenPoolInfos = await getTokenPoolInfos(connection, mint);
// 6. Select
const selectedTokenPoolInfos = selectTokenPoolInfosForDecompression(
tokenPoolInfos,
amount
);
// 7. Build instruction
const ix = await CompressedTokenProgram.decompress({
payer: payer.publicKey,
inputCompressedTokenAccounts: inputAccounts,
toAddress: ata.address,
amount,
tokenPoolInfos: selectedTokenPoolInfos,
recentInputStateRootIndices: proof.rootIndices,
recentValidityProof: proof.compressedProof,
});
// 8. Sign, send, and confirm
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);
})();
Set priority fees dynamically for decompression. Learn more here.
Native Swap via Jup-API
If you have a custom FE, you can let users swap compressed tokens using the Jup-API. A reference implementation is available here.Next Steps
Create an airdrop with claim functionality.
Customize token distribution and let users claim.
| distributor | simple-claim | |
|---|---|---|
| Vesting | Linear Vesting | Cliff at Slot X |
| Partial claims | Yes | No |
| Clawback | Yes | No |
| Frontend | REST API + CLI | None |
The programs are reference implementations and should be audited before production use.