@nosana/kit
Version:
Nosana KIT
343 lines • 15.9 kB
JavaScript
import { parseBase64RpcAccount, address, } from '@solana/kit';
import { NosanaError, ErrorCodes } from '../../../errors/NosanaError.js';
import * as programClient from '../../../generated_clients/merkle_distributor/index.js';
import { convertBigIntToNumber } from '../../../utils/index.js';
import { findAssociatedTokenPda, TOKEN_PROGRAM_ADDRESS } from '@solana-program/token';
import { SYSTEM_PROGRAM_ADDRESS } from '@solana-program/system';
import bs58 from 'bs58';
/**
* Claim target enum for merkle distributor.
* Determines which address receives the claimed tokens.
*/
export var ClaimTarget;
(function (ClaimTarget) {
ClaimTarget["YES"] = "YES";
ClaimTarget["NO"] = "NO";
})(ClaimTarget || (ClaimTarget = {}));
/**
* Allowed addresses for receiving claimed tokens from merkle distributor.
* The `to` account must be the ATA of one of these addresses.
*/
export const ALLOWED_RECEIVE_ADDRESSES = {
[ClaimTarget.YES]: address('YessuvqUauj9yW4B3eERcyRLWmQtWpFc2ERKmaedmCE'),
[ClaimTarget.NO]: address('NopXntmRdXhYNkoZaNTMUMShJ3aVG5RvwpiyPdd4bMh'),
};
/**
* Error thrown when a claim status account is not found
*/
export class ClaimStatusNotFoundError extends Error {
constructor(address) {
super(`Claim status account not found at address ${address}`);
this.name = 'ClaimStatusNotFoundError';
}
}
export function createMerkleDistributorProgram(deps, config) {
const programId = config.merkleDistributorAddress;
const client = programClient;
/**
* Transform merkle distributor account to include address and convert BigInt to numbers
*/
function transformMerkleDistributorAccount(distributorAccount) {
const { discriminator: _, root, buffer0, buffer1, buffer2, ...distributorAccountData } = distributorAccount.data;
const converted = convertBigIntToNumber(distributorAccountData);
return {
address: distributorAccount.address,
...converted,
root: bs58.encode(Buffer.from(root)),
buffer0: bs58.encode(Buffer.from(buffer0)),
buffer1: bs58.encode(Buffer.from(buffer1)),
buffer2: bs58.encode(Buffer.from(buffer2)),
};
}
/**
* Transform claim status account to include address and convert BigInt to numbers
*/
function transformClaimStatusAccount(claimStatusAccount) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { discriminator: _, ...claimStatusAccountData } = claimStatusAccount.data;
return {
address: claimStatusAccount.address,
...convertBigIntToNumber(claimStatusAccountData),
};
}
return {
/**
* Derive the ClaimStatus PDA address for a given distributor and optional claimant.
* If claimant is not provided, uses the wallet's address.
*
* @param distributor The address of the merkle distributor
* @param claimant Optional claimant address. If not provided, uses the wallet's address.
* @returns The ClaimStatus PDA address
* @throws Error if wallet is not set and claimant is not provided
*/
async getClaimStatusPda(distributor, claimant) {
let claimantAddress;
if (claimant) {
claimantAddress = claimant;
}
else {
const wallet = deps.getWallet();
if (!wallet) {
throw new Error('Wallet not set. Please set a wallet or provide a claimant address.');
}
claimantAddress = wallet.address;
}
return await deps.solana.pda(['ClaimStatus', claimantAddress, distributor], programId);
},
/**
* Fetch a merkle distributor account by address
*/
async get(addr) {
try {
const distributorAccount = await client.fetchMerkleDistributor(deps.solana.rpc, addr);
const distributor = transformMerkleDistributorAccount(distributorAccount);
return distributor;
}
catch (err) {
deps.logger.error(`Failed to fetch merkle distributor ${err}`);
throw err;
}
},
/**
* Fetch all merkle distributor accounts
*/
async all() {
try {
const getProgramAccountsResponse = await deps.solana.rpc
.getProgramAccounts(programId, {
encoding: 'base64',
filters: [
{
memcmp: {
offset: BigInt(0),
bytes: bs58.encode(Buffer.from(client.MERKLE_DISTRIBUTOR_DISCRIMINATOR)),
encoding: 'base58',
},
},
],
})
.send();
const distributors = getProgramAccountsResponse
.map((result) => {
try {
const distributorAccount = programClient.decodeMerkleDistributor(parseBase64RpcAccount(result.pubkey, result.account));
return transformMerkleDistributorAccount(distributorAccount);
}
catch (err) {
deps.logger.error(`Failed to decode merkle distributor ${err}`);
return null;
}
})
.filter((account) => account !== null);
return distributors;
}
catch (err) {
deps.logger.error(`Failed to fetch all merkle distributors ${err}`);
throw err;
}
},
/**
* Fetch a claim status account by address
*/
async getClaimStatus(addr) {
try {
const maybeClaimStatus = await client.fetchMaybeClaimStatus(deps.solana.rpc, addr);
// If account doesn't exist, throw a specific error
if (!maybeClaimStatus.exists) {
throw new ClaimStatusNotFoundError(addr);
}
// Transform and return the claim status
return transformClaimStatusAccount(maybeClaimStatus);
}
catch (err) {
deps.logger.error(`Failed to fetch claim status ${err}`);
throw err;
}
},
/**
* Fetch claim status for a specific distributor and optional claimant.
* Derives the ClaimStatus PDA using the claimant address (or wallet's address if not provided) and the distributor address.
*
* @param distributor The address of the merkle distributor
* @param claimant Optional claimant address. If not provided, uses the wallet's address.
* @returns The claim status if it exists, null otherwise
* @throws Error if wallet is not set and claimant is not provided
*/
async getClaimStatusForDistributor(distributor, claimant) {
try {
// Derive ClaimStatus PDA
const claimStatusPda = await this.getClaimStatusPda(distributor, claimant);
// Reuse getClaimStatus to fetch and transform the claim status
// If the account doesn't exist, it will throw, so we catch and return null
return await this.getClaimStatus(claimStatusPda);
}
catch (err) {
// If the account doesn't exist, return null instead of throwing
// Check if it's the specific ClaimStatusNotFoundError from getClaimStatus
if (err instanceof ClaimStatusNotFoundError) {
return null;
}
// For other errors, log and rethrow
deps.logger.error(`Failed to fetch claim status ${err}`);
throw err;
}
},
/**
* Fetch all claim status accounts
* TODO: add filter for claimant and distributor
*/
async allClaimStatus() {
try {
const getProgramAccountsResponse = await deps.solana.rpc
.getProgramAccounts(programId, {
encoding: 'base64',
filters: [
{
memcmp: {
offset: BigInt(0),
bytes: bs58.encode(Buffer.from(client.CLAIM_STATUS_DISCRIMINATOR)),
encoding: 'base58',
},
},
],
})
.send();
const claimStatuses = getProgramAccountsResponse
.map((result) => {
try {
const claimStatusAccount = programClient.decodeClaimStatus(parseBase64RpcAccount(result.pubkey, result.account));
return transformClaimStatusAccount(claimStatusAccount);
}
catch (err) {
deps.logger.error(`Failed to decode claim status ${err}`);
return null;
}
})
.filter((account) => account !== null);
return claimStatuses;
}
catch (err) {
deps.logger.error(`Failed to fetch all claim statuses ${err}`);
throw err;
}
},
/**
* Claim tokens from a merkle distributor.
* This function creates a new ClaimStatus account and claims the tokens in a single instruction.
*
* @param params Parameters for claiming tokens
* @param params.claimant Optional claimant signer. If not provided, uses the wallet.
* @returns The newClaim instruction
* @throws NosanaError if tokens have already been claimed
* @throws Error if wallet is not set and claimant is not provided
*/
async claim(params) {
// Determine claimant signer and address
let claimantSigner;
let claimantAddress;
if (params.claimant) {
claimantSigner = params.claimant;
claimantAddress = params.claimant.address;
}
else {
const wallet = deps.getWallet();
if (!wallet) {
throw new Error('Wallet not set. Please set a wallet or provide a claimant signer.');
}
claimantSigner = wallet;
claimantAddress = wallet.address;
}
try {
// Get the distributor account to find mint and tokenVault
const distributorAccount = await client.fetchMerkleDistributor(deps.solana.rpc, params.distributor);
// Derive ClaimStatus PDA using the claimant address
const claimStatusPda = await this.getClaimStatusPda(params.distributor, claimantAddress);
// Check if ClaimStatus account exists
const maybeClaimStatus = await client.fetchMaybeClaimStatus(deps.solana.rpc, claimStatusPda);
// If ClaimStatus already exists, throw an error (already claimed)
if (maybeClaimStatus.exists) {
throw new NosanaError('Tokens have already been claimed from this distributor', ErrorCodes.VALIDATION_ERROR);
}
// Get the target address for receiving tokens (YES or NO)
const targetAddress = ALLOWED_RECEIVE_ADDRESSES[params.target];
// Find the ATA of the target address (where tokens will be sent)
const [targetAta] = await findAssociatedTokenPda({
mint: distributorAccount.data.mint,
owner: targetAddress,
tokenProgram: TOKEN_PROGRAM_ADDRESS,
});
programClient.getClawbackInstruction({
distributor: params.distributor,
from: distributorAccount.data.tokenVault,
to: distributorAccount.data.clawbackReceiver,
claimant: claimantSigner,
tokenProgram: TOKEN_PROGRAM_ADDRESS,
systemProgram: SYSTEM_PROGRAM_ADDRESS,
});
// Create newClaim in struction which creates the account and claims the tokens
// Note: tokens go to the ATA of the target address (YES or NO), not the claimant's ATA
// The claimant in the instruction is the claimant signer (or wallet if not provided)
const newClaimInstruction = programClient.getNewClaimInstruction({
distributor: params.distributor,
claimStatus: claimStatusPda,
from: distributorAccount.data.tokenVault,
to: targetAta, // ATA of YES or NO address, not claimant's ATA
claimant: claimantSigner, // Claimant signer (or wallet if not provided)
tokenProgram: TOKEN_PROGRAM_ADDRESS,
systemProgram: SYSTEM_PROGRAM_ADDRESS,
amountUnlocked: params.amountUnlocked,
amountLocked: params.amountLocked,
proof: params.proof,
}, { programAddress: programId });
return newClaimInstruction;
}
catch (err) {
deps.logger.error(`Failed to create claim instructions: ${err}`);
throw err;
}
},
/**
* Clawback tokens from a merkle distributor.
* This function creates a clawback instruction to transfer tokens from the distributor's token vault to the clawback receiver.
*
* @param params Parameters for clawback
* @param params.distributor The address of the merkle distributor
* @param params.claimant Optional claimant signer. If not provided, uses the wallet.
* @returns The clawback instruction
* @throws Error if wallet is not set and claimant is not provided
*/
async clawback(params) {
// Determine claimant signer
let claimantSigner;
if (params.claimant) {
claimantSigner = params.claimant;
}
else {
const wallet = deps.getWallet();
if (!wallet) {
throw new Error('Wallet not set. Please set a wallet or provide a claimant signer.');
}
claimantSigner = wallet;
}
try {
// Get the distributor account to find tokenVault and clawbackReceiver
const distributorAccount = await client.fetchMerkleDistributor(deps.solana.rpc, params.distributor);
// Create clawback instruction
const clawbackInstruction = client.getClawbackInstruction({
distributor: params.distributor,
from: distributorAccount.data.tokenVault,
to: distributorAccount.data.clawbackReceiver,
claimant: claimantSigner,
tokenProgram: TOKEN_PROGRAM_ADDRESS,
systemProgram: SYSTEM_PROGRAM_ADDRESS,
}, { programAddress: programId });
return clawbackInstruction;
}
catch (err) {
deps.logger.error(`Failed to create clawback instruction: ${err}`);
throw err;
}
},
};
}
//# sourceMappingURL=MerkleDistributorProgram.js.map