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
JavaScript
"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;