jito-distributor-sdk
Version:
TypeScript SDK for JITO Merkle Distributor with production-ready versioning and double-hashing support
252 lines (212 loc) ⢠11 kB
text/typescript
import { config } from 'dotenv';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
import { getAssociatedTokenAddress, getAccount } from '@solana/spl-token';
import bs58 from 'bs58';
import fs from 'fs';
import { MerkleDistributor } from '../src/index';
// Import the JITO merkle tree implementation (double hashing)
import { createJitoMerkleTree, generateProofForRecipient, AirdropRecipient } from '../src/utils/merkle-tree';
// Load environment variables
config();
function getErrorMessage(error: any): string {
if (error instanceof Error) {
return error.message;
}
return String(error);
}
async function claimTokensV6Fixed() {
console.log('šÆ Claiming USDC Tokens from Merkle Distributor V6 (FIXED V2 Double Hashing)\n');
// Load distributor info
let distributorInfo: any;
try {
const infoFile = fs.readFileSync('./distributor-v6-fixed-v2-info.json', 'utf-8');
distributorInfo = JSON.parse(infoFile);
} catch (error) {
throw new Error('Could not load distributor-v6-fixed-v2-info.json. Make sure you have created the V6 distributor first.');
}
if (!distributorInfo.isFixedV2) {
throw new Error('This claim script is for FIXED V2 distributors only. The loaded distributor info does not have the V2 flag.');
}
// Validate environment variables
const privateKey = process.env.PRIVATE_KEY;
const rpcEndpoint = process.env.RPC_ENDPOINT || 'https://mainnet.helius-rpc.com/?api-key=616ef0ca-f8ff-499b-8ca2-cd967fb07ef2';
const walletAddress = process.env.WALLET_ADDRESS || "CoJebSiqLWbXmSCmSSis8NSjva4ag93isJV7dxcz8x5q";
if (!privateKey) {
throw new Error('PRIVATE_KEY not found in environment variables');
}
console.log('š Configuration:');
console.log(`RPC Endpoint: ${rpcEndpoint}`);
console.log(`Wallet Address: ${walletAddress}`);
console.log(`Distributor PDA: ${distributorInfo.distributorPDA}`);
console.log(`Mint: ${distributorInfo.mint}`);
console.log(`Merkle Root: ${distributorInfo.merkleRoot}`);
console.log(`Version: ${distributorInfo.version} (V6 - FIXED V2 Double Hashing)`);
// Setup connection and wallet
const connection = new Connection(rpcEndpoint, 'confirmed');
const keypair = Keypair.fromSecretKey(bs58.decode(privateKey));
const wallet = new Wallet(keypair);
const provider = new AnchorProvider(connection, wallet, { commitment: 'confirmed' });
console.log(`\nš Connected wallet: ${wallet.publicKey.toString()}`);
if (wallet.publicKey.toString() !== walletAddress) {
throw new Error('Wallet address mismatch! Check your PRIVATE_KEY and WALLET_ADDRESS');
}
// Initialize SDK
const sdk = new MerkleDistributor(provider);
const distributorPDA = new PublicKey(distributorInfo.distributorPDA);
const mint = new PublicKey(distributorInfo.mint);
// Get distributor account
console.log('\nš Checking distributor status...');
const distributor = await sdk.getDistributor(distributorPDA);
console.log('Distributor found!');
console.log(` Version: ${distributor.version.toString()}`);
console.log(` Root: ${Buffer.from(distributor.root).toString('hex')}`);
console.log(` Max Total Claim: ${distributor.maxTotalClaim.toString()}`);
console.log(` Total Claimed: ${distributor.totalAmountClaimed.toString()}`);
// Recreate the recipients list
const recipients: AirdropRecipient[] = [
{
address: new PublicKey("CoJebSiqLWbXmSCmSSis8NSjva4ag93isJV7dxcz8x5q"),
unlockedAmount: 1000000, // 1 USDC
lockedAmount: 0,
}
];
// Recreate the JITO merkle tree (double hashing)
console.log('\nš³ Recreating JITO (Double Hashing) Merkle Tree...');
const { tree } = createJitoMerkleTree(recipients);
const recreatedRoot = Buffer.from(tree.getRoot()).toString('hex');
console.log(`Recreated Root: ${recreatedRoot}`);
console.log(`Saved Root: ${distributorInfo.merkleRoot}`);
console.log(`Roots Match: ${recreatedRoot === distributorInfo.merkleRoot}`);
if (recreatedRoot !== distributorInfo.merkleRoot) {
throw new Error('Merkle root mismatch! The recreated V2 tree does not match the saved info.');
}
console.log('ā
V2 merkle tree recreation successful!');
// Generate JITO proof for the wallet
console.log('\nš Generating JITO (Double Hashing) Merkle Proof...');
const { proof, index, recipient } = generateProofForRecipient(tree, recipients, wallet.publicKey);
console.log(`Found recipient at index ${index}:`);
console.log(` Address: ${recipient.address.toString()}`);
console.log(` Unlocked: ${recipient.unlockedAmount / 1000000} USDC`);
console.log(` Locked: ${recipient.lockedAmount / 1000000} USDC`);
console.log(` Proof elements: ${proof.length}`);
if (proof.length > 0) {
console.log(` Proof: ${proof.map(p => Buffer.from(p).toString('hex'))}`);
} else {
console.log(` Proof: [] (empty - single recipient tree)`);
}
// Verify proof locally using FIXED V2 implementation
const rawLeafData = tree.getRawLeafForRecipient(recipient);
const isValidProof = tree.verifyProof(index, rawLeafData, proof.map(p => Buffer.from(p)));
if (!isValidProof) {
throw new Error('Generated FIXED V2 proof is invalid! There is still an issue with our V2 implementation.');
}
console.log('ā
FIXED V2 proof verified locally - should work with Rust program\'s double hashing!');
// Show detailed comparison with Rust program expectations
console.log('\nš¬ V2 Implementation Details:');
console.log('This exactly matches the Rust program\'s two-step process:');
console.log(` Raw Data: ${rawLeafData.toString('hex')}`);
const { createHash } = require('crypto');
const step1Hash = createHash('sha256').update(rawLeafData).digest();
const step2Hash = createHash('sha256').update(Buffer.concat([Buffer.from([0]), step1Hash])).digest();
console.log(` Step 1 (hashv raw): ${step1Hash.toString('hex')}`);
console.log(` Step 2 (+ prefix): ${step2Hash.toString('hex')}`);
console.log(` Tree Root: ${Buffer.from(tree.getRoot()).toString('hex')}`);
console.log(` Final Match: ${step2Hash.toString('hex') === Buffer.from(tree.getRoot()).toString('hex')}`);
// Check if already claimed
const [claimStatusPDA] = PublicKey.findProgramAddressSync(
[
Buffer.from('ClaimStatus'),
wallet.publicKey.toBuffer(),
distributorPDA.toBuffer()
],
sdk.programId
);
try {
const claimStatus = await sdk.getClaimStatus(claimStatusPDA);
console.log('\nā ļø Tokens already claimed!');
console.log(` Claimed by: ${claimStatus.claimant.toString()}`);
console.log(` Unlocked amount: ${claimStatus.unlockedAmount.toString()}`);
console.log(` Locked amount: ${claimStatus.lockedAmount.toString()}`);
return;
} catch (error) {
console.log('\nā
No previous claim found - proceeding with V6 claim...');
}
// Get or create claimant's token account
const claimantTokenAccount = await getAssociatedTokenAddress(mint, wallet.publicKey);
console.log('\nš° Token Account Setup:');
console.log(` Claimant Token Account: ${claimantTokenAccount.toString()}`);
try {
const accountInfo = await getAccount(connection, claimantTokenAccount);
console.log(` Current balance: ${accountInfo.amount.toString()}`);
} catch (error) {
console.log(' Account does not exist - will be created during claim');
}
try {
console.log('\nš Claiming tokens with FIXED V2 (Double Hashing) proof...');
console.log('š” This is the moment of truth - testing if double hashing resolves InvalidProof!');
const signature = await sdk.claim({
claimant: wallet.publicKey,
distributor: distributorPDA,
claimantTokenAccount: claimantTokenAccount,
amountUnlocked: BigInt(recipient.unlockedAmount),
amountLocked: BigInt(recipient.lockedAmount),
proof: proof
});
console.log(`ā
CLAIM SUCCESSFUL! InvalidProof error has been RESOLVED!`);
console.log(`Transaction signature: ${signature}`);
console.log(`View on Solana Explorer: https://explorer.solana.com/tx/${signature}?cluster=mainnet-beta`);
// Verify the claim
console.log('\nš Verifying successful claim...');
const finalClaimStatus = await sdk.getClaimStatus(claimStatusPDA);
console.log('Claim verification successful!');
console.log(` Claimant: ${finalClaimStatus.claimant.toString()}`);
console.log(` Unlocked Amount: ${finalClaimStatus.unlockedAmount.toString()}`);
console.log(` Locked Amount: ${finalClaimStatus.lockedAmount.toString()}`);
// Check final token balance
try {
const finalAccount = await getAccount(connection, claimantTokenAccount);
console.log(` Final token balance: ${finalAccount.amount.toString()} (${Number(finalAccount.amount) / 1000000} USDC)`);
} catch (error) {
console.log(' Could not fetch final balance');
}
console.log('\nššš ULTIMATE SUCCESS! ššš');
console.log('š§ The InvalidProof error has been COMPLETELY RESOLVED!');
console.log('š Root cause was the double hashing approach:');
console.log(' ā
JavaScript V2 now matches Rust program exactly');
console.log(' ā
Step 1: hash(claimant + amount_unlocked + amount_locked)');
console.log(' ā
Step 2: hash(LEAF_PREFIX + step1_result)');
console.log(' ā
Merkle proof validation now works perfectly');
console.log(' ā
Token transfer completed successfully');
console.log('\nš The merkle distributor system is now fully functional!');
} catch (error) {
console.error('\nā Error claiming USDC tokens:', getErrorMessage(error));
if (error instanceof Error) {
if (error.message.includes('InvalidProof')) {
console.log('\nš STILL getting InvalidProof error even with V2 double hashing!');
console.log(' This suggests there may be even deeper issues to investigate.');
console.log(' Possible remaining issues:');
console.log(' - Different hash function (our SHA256 vs Solana\'s hashv)');
console.log(' - Byte ordering or encoding differences');
console.log(' - Program ID or account derivation mismatches');
} else if (error.message.includes('ClaimExpired')) {
console.log('\nš” The claim window has expired.');
} else if (error.message.includes('already claimed')) {
console.log('\nš” Tokens have already been claimed.');
} else if (error.message.includes('insufficient funds')) {
console.log('\nš” Insufficient funds (check both SOL and token balances).');
}
}
throw error;
}
}
// Run the script
if (require.main === module) {
claimTokensV6Fixed()
.then(() => process.exit(0))
.catch((error) => {
console.error('Script failed:', error);
process.exit(1);
});
}
export { claimTokensV6Fixed };