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