UNPKG

jito-distributor-sdk

Version:

TypeScript SDK for JITO Merkle Distributor with production-ready versioning and double-hashing support

252 lines (212 loc) • 11 kB
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 };