UNPKG

jito-distributor-sdk

Version:

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

561 lines (560 loc) 23.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MerkleDistributor = void 0; const web3_js_1 = require("@solana/web3.js"); const anchor_1 = require("@coral-xyz/anchor"); const spl_token_1 = require("@solana/spl-token"); const merkle_distributor_json_1 = __importDefault(require("../idl/merkle_distributor.json")); const types_1 = require("./types"); const utils_1 = require("./utils"); /** * MerkleDistributor SDK class providing a clean interface to the Anchor program */ class MerkleDistributor { program; provider; programId; constructor(provider, programId) { this.provider = provider; this.programId = programId || types_1.PROGRAM_ID; this.program = new anchor_1.Program(merkle_distributor_json_1.default, this.programId, provider); } /** * Creates a new merkle distributor * @param args CreateDistributorArgs * @returns Transaction signature */ async createDistributor(args) { // Validate timestamps const timestampValidation = (0, utils_1.validateTimestamps)(args.startVestingTs, args.endVestingTs, args.clawbackStartTs); if (!timestampValidation.valid) { throw new Error(`Invalid timestamps: ${timestampValidation.error}`); } // Derive PDAs using the custom program ID const versionBuffer = Buffer.alloc(8); versionBuffer.writeBigUInt64LE(args.version); const [distributorPDA] = web3_js_1.PublicKey.findProgramAddressSync([ Buffer.from('MerkleDistributor'), args.mint.toBuffer(), versionBuffer ], this.programId); const distributorTokenAccount = await (0, spl_token_1.getAssociatedTokenAddress)(args.mint, distributorPDA, true // allowOwnerOffCurve ); // Convert arguments to the format expected by Anchor const anchorArgs = { version: (0, utils_1.bigintToBN)(args.version), root: Array.from(args.root), maxTotalClaim: (0, utils_1.bigintToBN)(args.maxTotalClaim), maxNumNodes: (0, utils_1.bigintToBN)(args.maxNumNodes), startVestingTs: (0, utils_1.bigintToBN)(args.startVestingTs), endVestingTs: (0, utils_1.bigintToBN)(args.endVestingTs), clawbackStartTs: (0, utils_1.bigintToBN)(args.clawbackStartTs), }; const signature = await this.program.methods .newDistributor(anchorArgs.version, anchorArgs.root, anchorArgs.maxTotalClaim, anchorArgs.maxNumNodes, anchorArgs.startVestingTs, anchorArgs.endVestingTs, anchorArgs.clawbackStartTs) .accounts({ distributor: distributorPDA, clawbackReceiver: args.clawbackReceiver, mint: args.mint, tokenVault: distributorTokenAccount, admin: args.admin, systemProgram: web3_js_1.SystemProgram.programId, associatedTokenProgram: spl_token_1.ASSOCIATED_TOKEN_PROGRAM_ID, tokenProgram: spl_token_1.TOKEN_PROGRAM_ID, }) .rpc(); return signature; } /** * Creates a transaction instruction for creating a new merkle distributor * @param args CreateDistributorArgs * @returns TransactionInstruction */ async createDistributorInstruction(args) { // Validate timestamps const timestampValidation = (0, utils_1.validateTimestamps)(args.startVestingTs, args.endVestingTs, args.clawbackStartTs); if (!timestampValidation.valid) { throw new Error(`Invalid timestamps: ${timestampValidation.error}`); } // Derive PDAs using the custom program ID const versionBuffer = Buffer.alloc(8); versionBuffer.writeBigUInt64LE(args.version); const [distributorPDA] = web3_js_1.PublicKey.findProgramAddressSync([ Buffer.from('MerkleDistributor'), args.mint.toBuffer(), versionBuffer ], this.programId); const distributorTokenAccount = await (0, spl_token_1.getAssociatedTokenAddress)(args.mint, distributorPDA, true // allowOwnerOffCurve ); // Convert arguments to the format expected by Anchor const anchorArgs = { version: (0, utils_1.bigintToBN)(args.version), root: Array.from(args.root), maxTotalClaim: (0, utils_1.bigintToBN)(args.maxTotalClaim), maxNumNodes: (0, utils_1.bigintToBN)(args.maxNumNodes), startVestingTs: (0, utils_1.bigintToBN)(args.startVestingTs), endVestingTs: (0, utils_1.bigintToBN)(args.endVestingTs), clawbackStartTs: (0, utils_1.bigintToBN)(args.clawbackStartTs), }; const instruction = await this.program.methods .newDistributor(anchorArgs.version, anchorArgs.root, anchorArgs.maxTotalClaim, anchorArgs.maxNumNodes, anchorArgs.startVestingTs, anchorArgs.endVestingTs, anchorArgs.clawbackStartTs) .accounts({ distributor: distributorPDA, clawbackReceiver: args.clawbackReceiver, mint: args.mint, tokenVault: distributorTokenAccount, admin: args.admin, systemProgram: web3_js_1.SystemProgram.programId, associatedTokenProgram: spl_token_1.ASSOCIATED_TOKEN_PROGRAM_ID, tokenProgram: spl_token_1.TOKEN_PROGRAM_ID, }) .instruction(); return instruction; } /** * Claims tokens from the distributor * @param args ClaimArgs * @returns Transaction signature */ async claim(args) { // Validate proof format if (!(0, utils_1.validateMerkleProof)(args.proof)) { throw new Error('Invalid merkle proof format'); } // Get distributor info to derive accounts const distributorInfo = await this.getDistributor(args.distributor); // Derive PDAs using the custom program ID const [claimStatusPDA] = web3_js_1.PublicKey.findProgramAddressSync([ Buffer.from('ClaimStatus'), args.claimant.toBuffer(), args.distributor.toBuffer() ], this.programId); const distributorTokenAccount = await (0, spl_token_1.getAssociatedTokenAddress)(distributorInfo.mint, args.distributor, true // allowOwnerOffCurve ); // Convert proof to the format expected by Anchor const anchorProof = args.proof.map(p => Array.from(p)); const signature = await this.program.methods .newClaim((0, utils_1.bigintToBN)(args.amountUnlocked), (0, utils_1.bigintToBN)(args.amountLocked), anchorProof) .accounts({ distributor: args.distributor, claimStatus: claimStatusPDA, from: distributorTokenAccount, to: args.claimantTokenAccount, claimant: args.claimant, tokenProgram: spl_token_1.TOKEN_PROGRAM_ID, systemProgram: web3_js_1.SystemProgram.programId, }) .rpc(); return signature; } /** * Creates a transaction instruction for claiming tokens * @param args ClaimArgs * @returns TransactionInstruction */ async claimInstruction(args) { // Validate proof format if (!(0, utils_1.validateMerkleProof)(args.proof)) { throw new Error('Invalid merkle proof format'); } // Get distributor info to derive accounts const distributorInfo = await this.getDistributor(args.distributor); // Derive PDAs using the custom program ID const [claimStatusPDA] = web3_js_1.PublicKey.findProgramAddressSync([ Buffer.from('ClaimStatus'), args.claimant.toBuffer(), args.distributor.toBuffer() ], this.programId); const distributorTokenAccount = await (0, spl_token_1.getAssociatedTokenAddress)(distributorInfo.mint, args.distributor, true // allowOwnerOffCurve ); // Convert proof to the format expected by Anchor const anchorProof = args.proof.map(p => Array.from(p)); const instruction = await this.program.methods .newClaim((0, utils_1.bigintToBN)(args.amountUnlocked), (0, utils_1.bigintToBN)(args.amountLocked), anchorProof) .accounts({ distributor: args.distributor, claimStatus: claimStatusPDA, from: distributorTokenAccount, to: args.claimantTokenAccount, claimant: args.claimant, tokenProgram: spl_token_1.TOKEN_PROGRAM_ID, systemProgram: web3_js_1.SystemProgram.programId, }) .instruction(); return instruction; } /** * Claims locked tokens after vesting period * @param args ClaimLockedArgs * @returns Transaction signature */ async claimLocked(args) { // Get distributor info to derive accounts const distributorInfo = await this.getDistributor(args.distributor); // Derive PDAs using the custom program ID const [claimStatusPDA] = web3_js_1.PublicKey.findProgramAddressSync([ Buffer.from('ClaimStatus'), args.claimant.toBuffer(), args.distributor.toBuffer() ], this.programId); const distributorTokenAccount = await (0, spl_token_1.getAssociatedTokenAddress)(distributorInfo.mint, args.distributor, true // allowOwnerOffCurve ); const signature = await this.program.methods .claimLocked() .accounts({ distributor: args.distributor, claimStatus: claimStatusPDA, from: distributorTokenAccount, to: args.claimantTokenAccount, claimant: args.claimant, tokenProgram: spl_token_1.TOKEN_PROGRAM_ID, }) .rpc(); return signature; } /** * Creates a transaction instruction for claiming locked tokens * @param args ClaimLockedArgs * @returns TransactionInstruction */ async claimLockedInstruction(args) { // Get distributor info to derive accounts const distributorInfo = await this.getDistributor(args.distributor); // Derive PDAs using the custom program ID const [claimStatusPDA] = web3_js_1.PublicKey.findProgramAddressSync([ Buffer.from('ClaimStatus'), args.claimant.toBuffer(), args.distributor.toBuffer() ], this.programId); const distributorTokenAccount = await (0, spl_token_1.getAssociatedTokenAddress)(distributorInfo.mint, args.distributor, true // allowOwnerOffCurve ); const instruction = await this.program.methods .claimLocked() .accounts({ distributor: args.distributor, claimStatus: claimStatusPDA, from: distributorTokenAccount, to: args.claimantTokenAccount, claimant: args.claimant, tokenProgram: spl_token_1.TOKEN_PROGRAM_ID, }) .instruction(); return instruction; } /** * Claws back remaining tokens to the clawback receiver * @param distributor Distributor public key * @param claimant Claimant public key (can be anyone after clawback period) * @returns Transaction signature */ async clawback(distributor, claimant) { // Get distributor info const distributorInfo = await this.getDistributor(distributor); const distributorTokenAccount = await (0, spl_token_1.getAssociatedTokenAddress)(distributorInfo.mint, distributor, true // allowOwnerOffCurve ); const clawbackTokenAccount = await (0, spl_token_1.getAssociatedTokenAddress)(distributorInfo.mint, distributorInfo.clawbackReceiver); const signature = await this.program.methods .clawback() .accounts({ distributor, from: distributorTokenAccount, to: clawbackTokenAccount, claimant, systemProgram: web3_js_1.SystemProgram.programId, tokenProgram: spl_token_1.TOKEN_PROGRAM_ID, }) .rpc(); return signature; } /** * Creates a transaction instruction for clawing back tokens * @param distributor Distributor public key * @param claimant Claimant public key (can be anyone after clawback period) * @returns TransactionInstruction */ async clawbackInstruction(distributor, claimant) { // Get distributor info const distributorInfo = await this.getDistributor(distributor); const distributorTokenAccount = await (0, spl_token_1.getAssociatedTokenAddress)(distributorInfo.mint, distributor, true // allowOwnerOffCurve ); const clawbackTokenAccount = await (0, spl_token_1.getAssociatedTokenAddress)(distributorInfo.mint, distributorInfo.clawbackReceiver); const instruction = await this.program.methods .clawback() .accounts({ distributor, from: distributorTokenAccount, to: clawbackTokenAccount, claimant, systemProgram: web3_js_1.SystemProgram.programId, tokenProgram: spl_token_1.TOKEN_PROGRAM_ID, }) .instruction(); return instruction; } /** * Sets a new admin for the distributor * @param distributor Distributor public key * @param currentAdmin Current admin public key * @param newAdmin New admin public key * @returns Transaction signature */ async setAdmin(distributor, currentAdmin, newAdmin) { const signature = await this.program.methods .setAdmin() .accounts({ distributor, admin: currentAdmin, newAdmin, }) .rpc(); return signature; } /** * Creates a transaction instruction for setting a new admin * @param distributor Distributor public key * @param currentAdmin Current admin public key * @param newAdmin New admin public key * @returns TransactionInstruction */ async setAdminInstruction(distributor, currentAdmin, newAdmin) { const instruction = await this.program.methods .setAdmin() .accounts({ distributor, admin: currentAdmin, newAdmin, }) .instruction(); return instruction; } /** * Sets a new clawback receiver for the distributor * @param distributor Distributor public key * @param newClawbackReceiver New clawback receiver public key * @param admin Admin public key * @returns Transaction signature */ async setClawbackReceiver(distributor, newClawbackReceiver, admin) { const signature = await this.program.methods .setClawbackReceiver() .accounts({ distributor, newClawbackAccount: newClawbackReceiver, admin, }) .rpc(); return signature; } /** * Creates a transaction instruction for setting a new clawback receiver * @param distributor Distributor public key * @param newClawbackReceiver New clawback receiver public key * @param admin Admin public key * @returns TransactionInstruction */ async setClawbackReceiverInstruction(distributor, newClawbackReceiver, admin) { const instruction = await this.program.methods .setClawbackReceiver() .accounts({ distributor, newClawbackAccount: newClawbackReceiver, admin, }) .instruction(); return instruction; } /** * Fetches a distributor account * @param distributor Distributor public key * @returns MerkleDistributor account data */ async getDistributor(distributor) { const account = await this.program.account.merkleDistributor.fetch(distributor); return account; } /** * Fetches a claim status account * @param claimStatus Claim status public key * @returns ClaimStatus account data */ async getClaimStatus(claimStatus) { const account = await this.program.account.claimStatus.fetch(claimStatus); return account; } /** * Fetches claim status for a specific claimant and distributor * @param claimant Claimant public key * @param distributor Distributor public key * @returns ClaimStatus account data or null if not found */ async getClaimStatusForClaimant(claimant, distributor) { try { const [claimStatusPDA] = web3_js_1.PublicKey.findProgramAddressSync([ Buffer.from('ClaimStatus'), claimant.toBuffer(), distributor.toBuffer() ], this.programId); return await this.getClaimStatus(claimStatusPDA); } catch (error) { // Account doesn't exist yet return null; } } /** * Checks if a claimant has already claimed * @param claimant Claimant public key * @param distributor Distributor public key * @returns Boolean indicating if tokens have been claimed */ async hasClaimed(claimant, distributor) { const claimStatus = await this.getClaimStatusForClaimant(claimant, distributor); return claimStatus !== null; } /** * Queries all existing distributors for a given mint * @param mint The mint to query distributors for * @param maxVersion Maximum version to check (default: 100) * @returns Map of version to distributor info */ async queryDistributorsForMint(mint, maxVersion = 100) { const distributors = new Map(); for (let version = 0n; version <= BigInt(maxVersion); version++) { try { const [pda] = (0, utils_1.getDistributorPDA)(mint, version); const account = await this.getDistributor(pda); distributors.set(version, { pda, account, version }); } catch (error) { // Distributor doesn't exist for this version, continue continue; } } return distributors; } /** * Finds the next available version for a mint * @param mint The mint to find next version for * @param startFrom Starting version to check from (default: 0) * @param maxCheck Maximum version to check (default: 1000) * @returns Next available version number */ async findNextAvailableVersion(mint, startFrom = 0n, maxCheck = 1000) { for (let version = startFrom; version <= BigInt(maxCheck); version++) { try { const [pda] = (0, utils_1.getDistributorPDA)(mint, version); await this.getDistributor(pda); // If we get here, distributor exists, continue to next version continue; } catch (error) { // Distributor doesn't exist, this version is available return version; } } throw new Error(`No available version found between ${startFrom} and ${maxCheck}`); } /** * Gets a comprehensive overview of distributions for a mint * @param mint The mint to get overview for * @param maxVersion Maximum version to check (default: 100) * @returns Distribution overview with used versions, next available, and stats */ async getDistributionOverview(mint, maxVersion = 100) { const distributors = await this.queryDistributorsForMint(mint, maxVersion); const usedVersions = Array.from(distributors.keys()).sort((a, b) => Number(a - b)); const nextAvailableVersion = await this.findNextAvailableVersion(mint, 0n, maxVersion + 100); let totalClaimed = 0n; let totalUnclaimed = 0n; const distributorMap = new Map(); for (const [version, info] of distributors) { const claimedAmount = BigInt(info.account.totalAmountClaimed.toString()); const maxClaim = BigInt(info.account.maxTotalClaim.toString()); const remainingAmount = maxClaim - claimedAmount; totalClaimed += claimedAmount; totalUnclaimed += remainingAmount; distributorMap.set(version, { ...info, claimedAmount, remainingAmount }); } return { mint, usedVersions, nextAvailableVersion, totalDistributors: distributors.size, totalClaimed, totalUnclaimed, distributors: distributorMap }; } /** * Checks if a version is available for a mint * @param mint The mint to check * @param version The version to check * @returns Boolean indicating if version is available */ async isVersionAvailable(mint, version) { try { const [pda] = (0, utils_1.getDistributorPDA)(mint, version); await this.getDistributor(pda); return false; // Distributor exists, version not available } catch (error) { return true; // Distributor doesn't exist, version available } } /** * Gets the PDA for a specific mint and version * @param mint The mint * @param version The version * @returns [PDA, bump] tuple */ getDistributorPDA(mint, version) { return (0, utils_1.getDistributorPDA)(mint, version); } /** * Batch check multiple versions for availability * @param mint The mint to check versions for * @param versions Array of versions to check * @returns Map of version to availability status */ async batchCheckVersions(mint, versions) { const results = new Map(); const checks = versions.map(async (version) => { const available = await this.isVersionAvailable(mint, version); results.set(version, available); }); await Promise.all(checks); return results; } /** * Builds a transaction with multiple instructions * @param instructions Array of transaction instructions to bundle * @returns Transaction ready to be signed and sent */ buildTransaction(instructions) { const transaction = new web3_js_1.Transaction(); instructions.forEach(ix => transaction.add(ix)); return transaction; } /** * Sends and confirms a transaction with multiple instructions * @param instructions Array of transaction instructions to bundle and send * @returns Transaction signature */ async sendTransaction(instructions) { const transaction = this.buildTransaction(instructions); const signature = await this.provider.sendAndConfirm(transaction); return signature; } } exports.MerkleDistributor = MerkleDistributor;