buy

Buy launchpad tokens with SOL or WSOL. The instruction supports exact-in and exact-out modes, applies protocol/creator fees, updates pool state, transfers tokens to the buyer, and emits a structured trade log (log_trade_internal) for indexers.

Program: Tradersdex Curve (Launchpad) Instruction: buy Emits: TradeEvent via internal log_trade_internal Pool status required: LIVE


Modes & Pool Types

Trade modes

Mode
Semantics

Exact-in (is_exact_in = true)

Spend up to sol_amount, must receive min_tokens_out

Exact-out (is_exact_in = false)

Receive exactly min_tokens_out, must spend sol_amount

Pool types (on-chain pool_type)

Value
Quote
Creator fee?

0 (Basic)

SOL

1 (WithCreator)

SOL

2 (WithCustomQuote)

WSOL

3 (Full)

WSOL

QuoteVault rule:

  • WSOL pools (2/3): quote_vault = ATA(NATIVE_MINT, pool)

  • SOL pools (0/1): quote_vault = pool (lamports live on the pool PDA)


Arguments

#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct BuyArgs {
    /// SOL/WSOL to spend (exact-in) or max-in (exact-out) in lamports
    pub sol_amount: u64,
    /// Min tokens out (exact-in) or exact tokens out (exact-out)
    pub min_tokens_out: u64,
    /// true => exact-in, false => exact-out
    pub is_exact_in: bool,
}

Accounts

#
Name
Writable
Seeds / Constraints
Description

0

pool

["pool", mint], owner = program

Pool PDA; custom-deserialized

1

mint

Sale token mint

2

quote_mint

For WSOL must be NATIVE_MINT

For SOL may be Pubkey::default()

3

token_vault

ATA(mint, pool) & amount > 0

Pool’s base token vault

4

quote_vault

WSOL: ATA(NATIVE_MINT, pool) • SOL: pool

Pool’s quote holding

5

user_token_account

ATA(mint, payer), init_if_needed

Buyer’s receiving ATA

6

user_quote_account

WSOL: ATA(NATIVE_MINT, payer)

Buyer’s WSOL ATA (unused for SOL)

7

fee_recipient

Must equal one of PDA(["protocol_fee_vault",[i]])

Protocol fee receiver

8

payer

Buyer

9

token_program

= anchor_spl::token::ID

SPL Token program

10

associated_token_program

For ATA ops

11

system_program

For SOL transfers

12

tradersdex_curve

= crate::ID

Self-check

13

event_authority

["event_authority"]

Signs inner log ix

14

token_creator

Must equal pool creator for fee pools

Creator pubkey

15

token_creator_ata

ATA(NATIVE_MINT, token_creator)

Needed to activate creator fee (WSOL path)


Behavior (summary)

  • Math

    • Uses virtual reserves: vr_quote, vr_token.

    • Exact-in: fee is taken from input; out = floor(rt * x_eff / (rb + x_eff)), capped by tokens_for_sell. If capped, recomputes the minimal gross-in needed for the cap.

    • Exact-out: solves minimal gross-in (after fees) to receive exactly min_tokens_out.

  • Fees

    • fee_bps = PROTOCOL_FEE_BPS (+ CREATOR_FEE_BPS if creator path is active)

    • Creator fee is active for pool types 1/3 and only if token_creator_ata is the correct WSOL ATA (and initialized).

    • Split: creator_fee = total_fee * CREATOR_FEE_BPS / fee_bps, protocol_fee = total_fee - creator_fee.

  • Indexer

    • Emits inner log_trade_internal with TradeLogData (pool, trader, mint, gross in, tokens out, fees, new reserves, timestamp).


Client recipe: Quote → Slippage → Build IX

1) Fetch & parse pool

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

const conn = new Connection(RPC_URL);
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);

const creatorFeeEligible = poolHasCreatorFee(pool);
// (Optionally also check that creator WSOL ATA exists to set creatorFeeActive=true for WSOL pools)

2) Quote (pick one path)

Exact-in quote

import { quoteBuyExactInFromPool, minOutWithSlippageFromPool } from "./utils";

const grossIn = 1_000_000n; // lamports/WSOL atoms you plan to spend
const q = quoteBuyExactInFromPool({
  pool,
  grossIn,
  protocolFeeBps: PROTOCOL_FEE_BPS,
  creatorFeeBps:  CREATOR_FEE_BPS,
  creatorFeeActive: creatorFeeEligible, // set true only when creator ATA is valid/initialized for WSOL
});

const minTokensOut = minOutWithSlippageFromPool(q.tokensOut, 100); // 1% slippage guard

Exact-out quote

import { quoteBuyExactOutFromPool, maxInWithSlippageFromPool } from "./utils";

const desiredOut = 50_000n; // exact tokens you want
const q = quoteBuyExactOutFromPool({
  pool,
  desiredOut,
  protocolFeeBps: PROTOCOL_FEE_BPS,
  creatorFeeBps:  CREATOR_FEE_BPS,
  creatorFeeActive: creatorFeeEligible,
});

const maxSolIn = maxInWithSlippageFromPool(q.grossInRequired, 100); // 1% slippage headroom

3) Build the instruction (no send)

import { Program, BN } from "@coral-xyz/anchor";
import {
  derivePoolPda,
  deriveEventAuthority,
  deriveProtocolFeeVault,
  ZERO_PUBKEY,
} from "./utils";
import {
  NATIVE_MINT,
  getAssociatedTokenAddressSync,
} from "@solana/spl-token";
import { SystemProgram, PublicKey } from "@solana/web3.js";

export async function buildBuyIx(
  program: Program,
  args: {
    payer: PublicKey;
    mint: PublicKey;
    poolType: 0 | 1 | 2 | 3;
    solAmount: bigint;      // exact-in amount or max-in for exact-out
    minTokensOut: bigint;   // min-out for exact-in or exact-out target
    isExactIn: boolean;
    feeVaultIndex: number;
    tokenCreator: PublicKey;
  }
) {
  const isWSOL = args.poolType === 2 || args.poolType === 3;
  const pool = derivePoolPda(program.programId, args.mint);

  const tokenVault      = getAssociatedTokenAddressSync(args.mint, pool, true);
  const quoteVault      = isWSOL ? getAssociatedTokenAddressSync(NATIVE_MINT, pool, true) : pool;
  const userTokenAta    = getAssociatedTokenAddressSync(args.mint, args.payer);
  const userQuoteAta    = getAssociatedTokenAddressSync(NATIVE_MINT, args.payer);
  const feeRecipient    = deriveProtocolFeeVault(args.feeVaultIndex, program.programId);
  const quoteMint       = isWSOL ? NATIVE_MINT : ZERO_PUBKEY;
  const tokenCreatorAta = getAssociatedTokenAddressSync(NATIVE_MINT, args.tokenCreator);

  const ix = await program.methods
    .buy({
      solAmount:     new BN(args.solAmount.toString()),
      minTokensOut:  new BN(args.minTokensOut.toString()),
      isExactIn:     args.isExactIn,
    })
    .accounts({
      pool,
      mint: args.mint,
      quoteMint,
      tokenVault,
      quoteVault,
      userTokenAccount:  userTokenAta,
      userQuoteAccount:  userQuoteAta,
      feeRecipient,
      payer: args.payer,
      tokenProgram:           new PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"),
      associatedTokenProgram: new PublicKey("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"),
      systemProgram:          SystemProgram.programId,
      tradersdex_curve:       program.programId,
      eventAuthority:         deriveEventAuthority(program.programId),
      tokenCreator:           args.tokenCreator,
      tokenCreatorAta,
    })
    .instruction();

  return { ix };
}

Putting it together

  • Exact-in send:

    • sol_amount = grossIn (your planned spend)

    • min_tokens_out = minTokensOut (slippage protected)

    • is_exact_in = true

  • Exact-out send:

    • sol_amount = maxSolIn (slippage padded cap)

    • min_tokens_out = desiredOut

    • is_exact_in = false


Fee vault PDAs

Protocol fee vaults are derived as:

protocol_fee_vault[i] = PDA(["protocol_fee_vault", [i]])

Use i ∈ [0..NUM_FEE_ACCOUNTS) and pass the derived PDA as fee_recipient.


Indexer: log_trade_internal

Every successful buy triggers an inner instruction carrying TradeLogData with:

  • pool, trader, mint

  • sol_amount (gross in)

  • token_amount

  • fee_amount, creator_fee, protocol_fee

  • is_buy = true

  • Post-trade: vr_quote, vr_token, raised_quote, sold_supply

  • timestamp

Consume these logs to index trades reliably.


Troubleshooting

  • InvalidWsolAccount (WSOL pools): ensure

    • quote_mint == NATIVE_MINT

    • quote_vault == ATA(NATIVE_MINT, pool)

    • user_quote_account == ATA(NATIVE_MINT, payer)

    • For creator-fee pools, token_creator_ata == ATA(NATIVE_MINT, token_creator)

  • InvalidFeeRecipient: derive one of protocol_fee_vault[i].

  • SlippageExceeded: relax min_tokens_out (exact-in) or increase sol_amount cap (exact-out).

  • PoolNotLive: target reached or sold out.

Last updated