jito-distributor-sdk
Version:
TypeScript SDK for JITO Merkle Distributor with production-ready versioning and double-hashing support
156 lines (139 loc) • 4.56 kB
text/typescript
import { PublicKey } from '@solana/web3.js';
import { PROGRAM_ID } from './types';
/**
* Seeds for PDA derivation
*/
const MERKLE_DISTRIBUTOR_SEED = 'MerkleDistributor';
const CLAIM_STATUS_SEED = 'ClaimStatus';
/**
* Derives the PDA for a MerkleDistributor account
* @param mint The mint public key
* @param version The version number
* @returns [PDA, bump] tuple
*/
export function getDistributorPDA(mint: PublicKey, version: bigint): [PublicKey, number] {
const versionBuffer = Buffer.alloc(8);
versionBuffer.writeBigUInt64LE(version);
return PublicKey.findProgramAddressSync(
[
Buffer.from(MERKLE_DISTRIBUTOR_SEED),
mint.toBuffer(),
versionBuffer,
],
PROGRAM_ID
);
}
/**
* Derives the PDA for a ClaimStatus account
* @param claimant The claimant's public key
* @param distributor The distributor's public key
* @returns [PDA, bump] tuple
*/
export function getClaimStatusPDA(claimant: PublicKey, distributor: PublicKey): [PublicKey, number] {
return PublicKey.findProgramAddressSync(
[
Buffer.from(CLAIM_STATUS_SEED),
claimant.toBuffer(),
distributor.toBuffer(),
],
PROGRAM_ID
);
}
/**
* Converts a hex string to Uint8Array
* @param hex Hex string (with or without 0x prefix)
* @returns Uint8Array
*/
export function hexToUint8Array(hex: string): Uint8Array {
const cleanHex = hex.startsWith('0x') ? hex.slice(2) : hex;
if (cleanHex.length % 2 !== 0) {
throw new Error('Hex string must have even length');
}
const bytes = new Uint8Array(cleanHex.length / 2);
for (let i = 0; i < cleanHex.length; i += 2) {
bytes[i / 2] = parseInt(cleanHex.substr(i, 2), 16);
}
return bytes;
}
/**
* Converts Uint8Array to hex string
* @param bytes Uint8Array
* @returns Hex string without 0x prefix
*/
export function uint8ArrayToHex(bytes: Uint8Array): string {
return Array.from(bytes)
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
/**
* Converts bigint to BN (Anchor's Big Number)
* @param value bigint value
* @returns BN
*/
export function bigintToBN(value: bigint): any {
const { BN } = require('@coral-xyz/anchor');
return new BN(value.toString());
}
/**
* Validates that a merkle proof is properly formatted
* @param proof Array of hex strings or Uint8Arrays
* @returns boolean
*/
export function validateMerkleProof(proof: (string | Uint8Array)[]): boolean {
for (const element of proof) {
if (typeof element === 'string') {
try {
const bytes = hexToUint8Array(element);
if (bytes.length !== 32) return false;
} catch {
return false;
}
} else if (element instanceof Uint8Array) {
if (element.length !== 32) return false;
} else {
return false;
}
}
return true;
}
/**
* Gets the current Unix timestamp
* @returns Current timestamp in seconds
*/
export function getCurrentTimestamp(): number {
return Math.floor(Date.now() / 1000);
}
/**
* Validates timestamp parameters for distributor creation
* @param startVestingTs Start vesting timestamp
* @param endVestingTs End vesting timestamp
* @param clawbackStartTs Clawback start timestamp
* @returns Object with validation result and error message if invalid
*/
export function validateTimestamps(
startVestingTs: bigint,
endVestingTs: bigint,
clawbackStartTs: bigint
): { valid: boolean; error?: string } {
const now = BigInt(getCurrentTimestamp());
const oneDay = BigInt(86400); // 24 hours in seconds
// Allow immediate distribution (current time) or future timestamps
if (startVestingTs < now - 60n) { // Allow 60 second tolerance for immediate distribution
return { valid: false, error: 'Start vesting timestamp cannot be in the past (beyond 60 seconds)' };
}
if (endVestingTs < now - 60n) { // Allow 60 second tolerance for immediate distribution
return { valid: false, error: 'End vesting timestamp cannot be in the past (beyond 60 seconds)' };
}
if (clawbackStartTs <= now) {
return { valid: false, error: 'Clawback start timestamp must be in the future' };
}
// Check if start is before end (or equal for immediate unlock)
if (startVestingTs > endVestingTs) {
return { valid: false, error: 'Start vesting must be before or equal to end vesting' };
}
// Check if clawback is at least one day after vesting ends
if (clawbackStartTs < endVestingTs + oneDay) {
return { valid: false, error: 'Clawback start must be at least one day after vesting ends' };
}
return { valid: true };
}