Utils (constants & derives)

Shared constants and helper functions used across the docs and examples.


Constants

// utils.ts
import { PublicKey } from "@solana/web3.js";
import { NATIVE_MINT, getAssociatedTokenAddressSync } from "@solana/spl-token";
import { BN } from "@coral-xyz/anchor";

/** ===== Program IDs ===== */
export const LAUNCHPAD_PROGRAM_ID = new PublicKey("REPLACE_WITH_PROGRAM_ID"); // Tradersdex Curve (Launchpad)
export const TOKEN_PROGRAM_ID     = new PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
export const ATOKEN_PROGRAM_ID    = new PublicKey("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL");
export const SYSTEM_PROGRAM_ID    = new PublicKey("11111111111111111111111111111111");
export const MPL_PROGRAM_ID       = new PublicKey("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"); // Metaplex Token Metadata

/** ===== Fees / math ===== */
export const TEN_K = 10_000n; // 10000 (basis points denominator)

/** If you want these visible to client code, mirror your on-chain constants here. */
export const PROTOCOL_FEE_BPS =  /* e.g. */ 75; // 0.75% 
export const CREATOR_FEE_BPS  =  /* e.g. */ 25; // 0.25%  
export const NUM_FEE_ACCOUNTS =  /* e.g. */ 10;   // how many protocol fee PDAs exist

/** ===== Pool types (curveType) =====
 *  0: SOL  / no creator fee
 *  1: SOL  / creator fee
 *  2: WSOL / no creator fee
 *  3: WSOL / creator fee
 */
export type PoolType = 0 | 1 | 2 | 3;

/** ===== Pool status (for docs/reference) ===== */
export const POOL_STATUS_LIVE      = 1;
export const POOL_STATUS_COMPLETED = 2;

/** ===== Misc ===== */
export const ZERO_PUBKEY = new PublicKey(new Uint8Array(32)); // Pubkey::default() in Rust

Derive functions

// utils.ts (continued)

/** u128 little-endian → Buffer(16) */
export function le128(n: bigint): Buffer {
  const b = Buffer.alloc(16);
  let x = n;
  for (let i = 0; i < 16; i++) { b[i] = Number(x & 0xffn); x >>= 8n; }
  return b;
}

/** Buffer(16) (LE) → bigint */
export function fromLe128(buf: Buffer): bigint {
  if (buf.length !== 16) throw new Error("fromLe128: expected 16 bytes");
  let x = 0n;
  for (let i = 15; i >= 0; i--) x = (x << 8n) | BigInt(buf[i]);
  return x;
}

/** Random u128 bigint */
export function randomU128(): bigint {
  // Using Node crypto to make a 16-byte random; convert as LE or BE consistently with le128
  return BigInt("0x" + require("crypto").randomBytes(16).toString("hex"));
}

/** PDA(["mint", le_u128(salt)]) under Launchpad program */
export function deriveMintPda(salt: bigint, programId = LAUNCHPAD_PROGRAM_ID): PublicKey {
  return PublicKey.findProgramAddressSync([Buffer.from("mint"), le128(salt)], programId)[0];
}

/** PDA(["pool", mint]) under Launchpad program */
export function derivePoolPda(mint: PublicKey, programId = LAUNCHPAD_PROGRAM_ID): PublicKey {
  return PublicKey.findProgramAddressSync([Buffer.from("pool"), mint.toBuffer()], programId)[0];
}

/** PDA(["token_authority"]) under Launchpad program */
export function derivePoolSigner(programId = LAUNCHPAD_PROGRAM_ID): PublicKey {
  return PublicKey.findProgramAddressSync([Buffer.from("token_authority")], programId)[0];
}

/** PDA(["event_authority"]) under Launchpad program */
export function deriveEventAuthority(programId = LAUNCHPAD_PROGRAM_ID): PublicKey {
  return PublicKey.findProgramAddressSync([Buffer.from("event_authority")], programId)[0];
}

/** PDA(["protocol_fee_vault", [i]]) under Launchpad program */
export function deriveProtocolFeeVault(index: number, programId = LAUNCHPAD_PROGRAM_ID): PublicKey {
  if (index < 0 || index >= NUM_FEE_ACCOUNTS) {
    throw new Error(`deriveProtocolFeeVault: index ${index} out of range (0..${NUM_FEE_ACCOUNTS - 1})`);
  }
  return PublicKey.findProgramAddressSync(
    [Buffer.from("protocol_fee_vault"), Buffer.from([index])],
    programId
  )[0];
}

/** MPL metadata PDA for a mint: ["metadata", MPL_ID, mint] on MPL program */
export function deriveMetadataPda(mint: PublicKey): PublicKey {
  return PublicKey.findProgramAddressSync(
    [Buffer.from("metadata"), MPL_PROGRAM_ID.toBuffer(), mint.toBuffer()],
    MPL_PROGRAM_ID
  )[0];
}

/** Pool’s base token vault = ATA(mint, pool), off-curve owner allowed */
export function deriveTokenVault(mint: PublicKey, pool: PublicKey): PublicKey {
  return getAssociatedTokenAddressSync(mint, pool, true);
}

/** Quote vault rule:
 *  - WSOL:   return ATA(NATIVE_MINT, pool)
 *  - non-WSOL: return the pool account itself (program uses lamports on SOL path)
 */
export function deriveQuoteVault(isWSOL: boolean, pool: PublicKey): PublicKey {
  return isWSOL ? getAssociatedTokenAddressSync(NATIVE_MINT, pool, true) : pool;
}

/** User’s base token ATA */
export function deriveUserTokenAccount(mint: PublicKey, owner: PublicKey): PublicKey {
  return getAssociatedTokenAddressSync(mint, owner);
}

/** User’s WSOL ATA (used only on WSOL path) */
export function deriveUserWsolAta(owner: PublicKey): PublicKey {
  return getAssociatedTokenAddressSync(NATIVE_MINT, owner);
}

Curve type helpers

// utils.ts (continued)

/** True if pool type is a WSOL quote path */
export function isWsolPoolType(poolType: PoolType): boolean {
  return poolType === 2 || poolType === 3;
}

/** Compute curveType from quote mint & creator-fee flag.
 *  Mapping:
 *   0: SOL  / no creator fee
 *   1: SOL  / creator fee
 *   2: WSOL / no creator fee
 *   3: WSOL / creator fee
 *
 *  NOTE: We treat NATIVE_MINT as WSOL branch. For SOL branch, pass quoteMint=ZERO_PUBKEY.
 */
export function curveTypeFrom(quoteMint: PublicKey, creatorFeeEnabled: boolean): PoolType {
  const isWSOL = quoteMint.equals(NATIVE_MINT);
  return ((isWSOL ? 2 : 0) + (creatorFeeEnabled ? 1 : 0)) as PoolType;
}

Validation helpers (mirror on-chain rules)

// utils.ts (continued)

/** Enforce doc-side rules to fail fast before sending the tx.
 *  - decimals must be 6
 *  - name.length >= 2
 *  - symbol.length >= 2
 *  - if uri non-empty, it must start with "ipfs://"
 */
export function assertLaunchpadMetadataRules(params: {
  decimals: number;
  name: string;
  symbol: string;
  uri: string;
}) {
  const { decimals, name, symbol, uri } = params;
  if (decimals !== 6) throw new Error("decimals must be 6");
  if (!name || name.length < 2) throw new Error("name must be at least 2 characters");
  if (!symbol || symbol.length < 2) throw new Error("symbol must be at least 2 characters");
  if (uri.length > 0 && !uri.startsWith("ipfs://")) {
    throw new Error("uri must start with ipfs:// when provided");
  }
}

/** BN helpers */
export const toBNu64  = (n: bigint | number | string) => new BN(n.toString());  // for u64 args
export const toBNu128 = (n: bigint | number | string) => new BN(n.toString());  // for u128 args

Example: fee vault selection

// Pick a protocol fee vault by index (0..NUM_FEE_ACCOUNTS-1)
export function selectFeeVault(index = 0, programId = LAUNCHPAD_PROGRAM_ID): PublicKey {
  return deriveProtocolFeeVault(index, programId);
}

Import pattern

// In your client code:
import {
  LAUNCHPAD_PROGRAM_ID,
  TOKEN_PROGRAM_ID,
  ATOKEN_PROGRAM_ID,
  MPL_PROGRAM_ID,
  NATIVE_MINT,
  ZERO_PUBKEY,
  TEN_K,
  PROTOCOL_FEE_BPS,
  CREATOR_FEE_BPS,
  NUM_FEE_ACCOUNTS,
  type PoolType,
  le128, fromLe128, randomU128,
  deriveMintPda, derivePoolPda, derivePoolSigner, deriveEventAuthority,
  deriveProtocolFeeVault, deriveMetadataPda,
  deriveTokenVault, deriveQuoteVault, deriveUserTokenAccount, deriveUserWsolAta,
  isWsolPoolType, curveTypeFrom,
  assertLaunchpadMetadataRules,
  toBNu64, toBNu128,
} from "./utils";

Pool parsing (from on-chain account bytes)

// utils.ts (append)

import { PublicKey, Connection } from "@solana/web3.js";

/** Mirror of your Rust enum (u8) */
export enum PoolTypeEnum {
  Basic = 0,
  WithCreator = 1,
  WithCustomQuote = 2,
  Full = 3,
}

/** Parsed, pool-agnostic view (common fields + optionals) */
export type PoolParsed = {
  poolType: PoolTypeEnum;
  status: number;            // u8
  mintSalt: bigint;          // u128
  vrToken: bigint;           // u64
  vrQuote: bigint;           // u64
  soldSupply: bigint;        // u64
  raisedQuote: bigint;       // u64
  targetRaised: bigint;      // u64
  tokensForSell: bigint;     // u64
  // optional:
  creator?: PublicKey;
  quoteMint?: PublicKey;
  // raw:
  dataLen: number;
};

const U64 = (b: Buffer, o: number) => BigInt.asUintN(64, b.readBigUInt64LE(o));
const U128 = (b: Buffer, o: number) =>
  BigInt.asUintN(
    128,
    (BigInt(b.readBigUInt64LE(o + 8)) << 64n) | BigInt(b.readBigUInt64LE(o))
  );

const P32 = (b: Buffer, o: number) => new PublicKey(b.subarray(o, o + 32));

/** Sizes from your Rust impls (no Anchor discriminator assumed since your structs don’t include it) */
export const POOL_BASIC_SIZE = 1 + 1 + 16 + 8 + 8 + 8 + 8 + 8 + 8;         // 50
export const POOL_CREATOR_SIZE = POOL_BASIC_SIZE + 32;                      // 82
export const POOL_CUSTOM_QUOTE_SIZE = POOL_BASIC_SIZE + 32;                 // 82
export const POOL_FULL_SIZE = POOL_BASIC_SIZE + 32 + 32;                    // 114

/** Parse pool account data exactly as in your Rust layouts. */
export function parsePoolAccount(data: Buffer): PoolParsed {
  if (!data || data.length < POOL_BASIC_SIZE) {
    throw new Error("parsePoolAccount: data too small");
  }
  const poolType = data[0] as PoolTypeEnum;
  const status = data[1];
  const mintSalt = U128(data, 2);
  const vrToken = U64(data, 18);
  const vrQuote = U64(data, 26);
  const soldSupply = U64(data, 34);
  const raisedQuote = U64(data, 42);
  const targetRaised = U64(data, 50);
  const tokensForSell = U64(data, 58);

  if (poolType === PoolTypeEnum.Basic) {
    if (data.length < POOL_BASIC_SIZE) throw new Error("parsePoolAccount: Basic size mismatch");
    return {
      poolType, status, mintSalt, vrToken, vrQuote, soldSupply, raisedQuote, targetRaised, tokensForSell,
      dataLen: data.length,
    };
  }
  if (poolType === PoolTypeEnum.WithCreator) {
    if (data.length < POOL_CREATOR_SIZE) throw new Error("parsePoolAccount: WithCreator size mismatch");
    const creator = P32(data, 66);
    return {
      poolType, status, mintSalt, vrToken, vrQuote, soldSupply, raisedQuote, targetRaised, tokensForSell,
      creator, dataLen: data.length,
    };
  }
  if (poolType === PoolTypeEnum.WithCustomQuote) {
    if (data.length < POOL_CUSTOM_QUOTE_SIZE) throw new Error("parsePoolAccount: WithCustomQuote size mismatch");
    const quoteMint = P32(data, 66);
    return {
      poolType, status, mintSalt, vrToken, vrQuote, soldSupply, raisedQuote, targetRaised, tokensForSell,
      quoteMint, dataLen: data.length,
    };
  }
  if (poolType === PoolTypeEnum.Full) {
    if (data.length < POOL_FULL_SIZE) throw new Error("parsePoolAccount: Full size mismatch");
    const creator = P32(data, 66);
    const quoteMint = P32(data, 98);
    return {
      poolType, status, mintSalt, vrToken, vrQuote, soldSupply, raisedQuote, targetRaised, tokensForSell,
      creator, quoteMint, dataLen: data.length,
    };
  }
  throw new Error(`parsePoolAccount: Invalid pool_type=${poolType}`);
}

/** Helper: WSOL path if pool_type is 2 or 3 (matches your on-chain check) */
export const poolIsWsol = (p: PoolParsed) =>
  p.poolType === PoolTypeEnum.WithCustomQuote || p.poolType === PoolTypeEnum.Full;

/** Helper: pool supports creator fee if pool_type is 1 or 3 */
export const poolHasCreatorFee = (p: PoolParsed) =>
  p.poolType === PoolTypeEnum.WithCreator || p.poolType === PoolTypeEnum.Full;

Pool-aware math (quotes & slippage)

// utils.ts (append below parse helpers)

// ----- bigint math (same as before) -----
export const TEN_K_BIG = 10_000n;
export function mulDivFloor(a: bigint, b: bigint, c: bigint): bigint {
  if (c === 0n) throw new Error("mulDivFloor: division by zero");
  return (a * b) / c;
}
export function mulDivCeil(a: bigint, b: bigint, c: bigint): bigint {
  if (c === 0n) throw new Error("mulDivCeil: division by zero");
  const prod = a * b;
  return (prod + (c - 1n)) / c;
}

export type FeeQuote = {
  totalFee: bigint;
  protocolFee: bigint;
  creatorFee: bigint;
  feeBpsApplied: number;
};

export function splitFees(totalFee: bigint, protocolFeeBps: number, creatorFeeBps: number, creatorFeeActive: boolean): FeeQuote {
  const feeBpsApplied = protocolFeeBps + (creatorFeeActive ? creatorFeeBps : 0);
  if (feeBpsApplied <= 0) return { totalFee, protocolFee: 0n, creatorFee: 0n, feeBpsApplied: 0 };
  const creatorFee = creatorFeeActive
    ? mulDivFloor(totalFee, BigInt(creatorFeeBps), BigInt(feeBpsApplied))
    : 0n;
  const protocolFee = totalFee - creatorFee;
  return { totalFee, protocolFee, creatorFee, feeBpsApplied };
}

// ----- POOL-AWARE QUOTES -----

export type PoolExactInParams = {
  pool: PoolParsed;               // parsed pool
  grossIn: bigint;                // lamports/atoms user will spend
  protocolFeeBps: number;
  creatorFeeBps: number;
  creatorFeeActive: boolean;      // set true iff (poolHasCreatorFee && WSOL creator ATA is initialized)
};
export type PoolExactInQuote = {
  tokensOut: bigint;
  grossInUsed: bigint;
  poolAmount: bigint;
  fee: FeeQuote;
  wouldCap: boolean;
  wouldFailCap: boolean;
};

export function quoteBuyExactInFromPool(p: PoolExactInParams): PoolExactInQuote {
  const rb = p.pool.vrQuote;
  const rt = p.pool.vrToken;
  const maxOut = p.pool.tokensForSell;

  const feeBpsApplied = p.protocolFeeBps + (p.creatorFeeActive ? p.creatorFeeBps : 0);
  const feeGuess = mulDivCeil(p.grossIn, BigInt(feeBpsApplied), TEN_K_BIG);
  const xEff = p.grossIn - feeGuess;
  if (xEff < 0n) throw new Error("quoteBuyExactInFromPool: negative effective input");

  const denom = rb + xEff;
  if (denom === 0n) throw new Error("quoteBuyExactInFromPool: invalid reserves");
  const outEst = mulDivFloor(rt, xEff, denom);

  if (outEst > maxOut) {
    // cap branch (same algebra as on-chain)
    const denom2 = rt - maxOut;
    if (denom2 <= 0n) throw new Error("quoteBuyExactInFromPool: insufficient tokens");
    const xEffNeeded = mulDivCeil(maxOut, rb, denom2);
    const feeDen = TEN_K_BIG - BigInt(feeBpsApplied);
    if (feeDen <= 0n) throw new Error("quoteBuyExactInFromPool: fee too large");
    const grossNeeded = mulDivCeil(xEffNeeded, TEN_K_BIG, feeDen);
    const wouldFailCap = grossNeeded > p.grossIn;

    const totalFee = grossNeeded - xEffNeeded;
    const fee = splitFees(totalFee, p.protocolFeeBps, p.creatorFeeBps, p.creatorFeeActive);
    const poolAmount = grossNeeded - totalFee;

    return {
      tokensOut: maxOut,
      grossInUsed: grossNeeded,
      poolAmount,
      fee,
      wouldCap: true,
      wouldFailCap,
    };
  }

  const fee = splitFees(feeGuess, p.protocolFeeBps, p.creatorFeeBps, p.creatorFeeActive);
  const poolAmount = p.grossIn - feeGuess;

  return {
    tokensOut: outEst,
    grossInUsed: p.grossIn,
    poolAmount,
    fee,
    wouldCap: false,
    wouldFailCap: false,
  };
}

export type PoolExactOutParams = {
  pool: PoolParsed;
  desiredOut: bigint;            // exact tokens wanted
  protocolFeeBps: number;
  creatorFeeBps: number;
  creatorFeeActive: boolean;
};
export type PoolExactOutQuote = {
  grossInRequired: bigint;
  poolAmount: bigint;
  fee: FeeQuote;
  wouldExceedAvailability: boolean;
};

export function quoteBuyExactOutFromPool(p: PoolExactOutParams): PoolExactOutQuote {
  const rb = p.pool.vrQuote;
  const rt = p.pool.vrToken;

  const wouldExceedAvailability = p.desiredOut > p.pool.tokensForSell;
  if (p.desiredOut <= 0n) throw new Error("quoteBuyExactOutFromPool: desiredOut must be > 0");

  const denom = rt - p.desiredOut;
  if (denom <= 0n) throw new Error("quoteBuyExactOutFromPool: insufficient liquidity (rt <= desiredOut)");
  const xEffNeeded = mulDivCeil(p.desiredOut, rb, denom);

  const feeBpsApplied = p.protocolFeeBps + (p.creatorFeeActive ? p.creatorFeeBps : 0);
  const feeDen = TEN_K_BIG - BigInt(feeBpsApplied);
  if (feeDen <= 0n) throw new Error("quoteBuyExactOutFromPool: fee too large");
  const grossInRequired = mulDivCeil(xEffNeeded, TEN_K_BIG, feeDen);

  const totalFee = grossInRequired - xEffNeeded;
  const fee = splitFees(totalFee, p.protocolFeeBps, p.creatorFeeBps, p.creatorFeeActive);
  const poolAmount = grossInRequired - totalFee;

  return { grossInRequired, poolAmount, fee, wouldExceedAvailability };
}

// ----- slippage wrappers (pool-aware) -----

/** Exact-in: output floor after slippage bps */
export function minOutWithSlippageFromPool(tokensOutEstimate: bigint, slippageBps: number): bigint {
  if (slippageBps < 0) throw new Error("minOutWithSlippageFromPool: negative bps");
  const keepBps = TEN_K_BIG - BigInt(slippageBps);
  return mulDivFloor(tokensOutEstimate, keepBps, TEN_K_BIG);
}

/** Exact-out: max gross-in after slippage bps */
export function maxInWithSlippageFromPool(grossInRequired: bigint, slippageBps: number): bigint {
  if (slippageBps < 0) throw new Error("maxInWithSlippageFromPool: negative bps");
  const addBps = TEN_K_BIG + BigInt(slippageBps);
  return mulDivCeil(grossInRequired, addBps, TEN_K_BIG);
}

Quick usage example

// Fetch and parse the pool, then quote

import { Connection, PublicKey } from "@solana/web3.js";
import {
  parsePoolAccount,
  quoteBuyExactInFromPool,
  quoteBuyExactOutFromPool,
  minOutWithSlippageFromPool,
  maxInWithSlippageFromPool,
  PROTOCOL_FEE_BPS,
  CREATOR_FEE_BPS,
  poolHasCreatorFee,
  poolIsWsol,
} from "./utils";

const conn = new Connection("https://api.mainnet-beta.solana.com");
const poolPk = new PublicKey("POOL_PDA_HERE");

const ai = await conn.getAccountInfo(poolPk);
if (!ai) throw new Error("Pool not found");
const pool = parsePoolAccount(ai.data);

// Decide if creator fee is active (pool types 1/3) AND creator WSOL ATA exists/initialized on WSOL pools.
// Here we just use the pool type gate; wire your ATA existence check as needed:
const creatorFeeActive = poolHasCreatorFee(pool);

// ---- Exact-in quote ----
const qIn = quoteBuyExactInFromPool({
  pool,
  grossIn: 1_000_000n, // lamports/WSOL atoms
  protocolFeeBps: PROTOCOL_FEE_BPS,
  creatorFeeBps: CREATOR_FEE_BPS,
  creatorFeeActive,
});
const minOut = minOutWithSlippageFromPool(qIn.tokensOut, 100); // 1%

// ---- Exact-out quote ----
const qOut = quoteBuyExactOutFromPool({
  pool,
  desiredOut: 50_000n,
  protocolFeeBps: PROTOCOL_FEE_BPS,
  creatorFeeBps: CREATOR_FEE_BPS,
  creatorFeeActive,
});
const maxIn = maxInWithSlippageFromPool(qOut.grossInRequired, 100); // 1%

Last updated