Skip to content

Latest commit

 

History

History
319 lines (247 loc) · 13.4 KB

File metadata and controls

319 lines (247 loc) · 13.4 KB

Practical Guide: Testing and Using buy_random_bag (Anchor + Raydium CLMM)

This guide explains, step by step and without assuming prior Solana knowledge, how to invoke the buy_random_bag instruction from this repo's Anchor program. It covers:

  • Which accounts are required and why
  • The parameters to send and how to compute them
  • What the method does internally (transfers, fees, Raydium swaps)
  • How to build Raydium CLMM "remaining accounts"
  • A minimal TypeScript example (frontend) and how to run local tests

1) Functional overview

buy_random_bag receives SOL from the buyer, takes a protocol fee, wraps the remainder into WSOL, and executes a series of swaps (one or more) on Raydium CLMM to buy a diversified “bag” of tokens. It emits a summary event and increases the protocol fee counter in the global state.

2) Signature, parameters, and required accounts

On-chain signature

pub fn buy_random_bag<'a, 'b, 'c: 'info, 'info>(
    ctx: Context<'a, 'b, 'c, 'info, BuyRandomBag<'info>>,
    amount: u64,                 // SOL in lamports contributed by buyer
    total_swaps: u8,             // how many swaps to perform (how many output tokens)
    prices_scaled: Vec<u128>,    // vector of scaled prices (see section 4)
    tick_array_amount: Vec<u8>,  // number of tick accounts per swap
) -> Result<()>

Note: If you see an old IDL in idls/randombag.json with different args, ignore it. Use the above signature (the one used by tests and target/types).

Accounts (struct BuyRandomBag)

  • buyer: Signer (writable) — Your wallet; pays SOL and signs.
  • global_state (PDA) — Protocol global state.
    • seeds: b"global_state"
  • vault (PDA) — SystemAccount vault receiving SOL from buyer.
    • seeds: b"connected"
  • wsol_vault (PDA, TokenAccount) — WSOL SPL account owned by vault.
    • seeds: [b"connected", wsol_mint]
    • token::authority = vault
  • wsol_mint: WSOL mint (So1111... on devnet).
  • clmm_program: Raydium CLMM program (devnet: DRayAUgEN...).
  • token_program_2022: SPL Token 2022 program.
  • memo_program: Memo program.
  • associated_token_program, token_program, system_program: Standard programs.
  • remaining_accounts: Dynamic array of accounts needed for each Raydium swap (see section 3).

3) Raydium remaining accounts per swap

For each swap, swap_batch expects a block of accounts in this order:

  1. input_vault (mut) — Pool vault for input token (WSOL).
  2. output_vault (mut) — Pool vault for output token.
  3. output_mint — Output token mint.
  4. pool_state (mut) — CLMM pool account.
  5. observation_state (mut) — Pool oracle observation account.
  6. amm_config — AMM config (Raydium PDA).
  7. output_token_account (mut) — Buyer's ATA for the output token.
  8. …extra tick arrays (mut) — Tick accounts required to route the swap. The count is tick_array_amount[i].

So, per swap: 7 + tick_array_amount[i] accounts.

How to obtain these from the frontend: the repo includes a test helper you can copy/adapt:

  • tests/utils/raydium.tsgenerateRaydiumAccountsPerTokens(...)
    • Resolves the pool with Raydium SDK (devnet), computes pool accounts, ammConfig, observation, vaults, computes a reference price and collects tick arrays.
    • Returns: { remainingAccounts, priceScaled, tickArrayAmount }.

4) Computing prices_scaled and tolerances

  • prices_scaled[i] is the pool price scaled by 10^12 (see PRICE_SCALE = 12). In tests it is computed from Raydium’s public API:
    • priceScaled = floor(poolPrice * 10^12)
  • On-chain, per swap we derive:
    • other_amount_threshold = Minimum expected output, adjusted for decimals and slippage.
      • Uses calculate_other_amount_threshold(amount_in, price_scaled, decimals_in, decimals_out).
      • Internal fixed slippage: SLIPPAGE_BPS = 200 (2%).
    • sqrt_price_limit_x64 = Price limit (sqrt) around the current pool price, with 2% margin (calculate_sqrt_price_limit_x64).

You don’t need to send those last two; they’re computed inside the program using prices_scaled and pool data.

5) Internal flow (what happens on-chain)

Inside process_buy_random_bag:

  1. Transfers amount SOL from buyer to vault (System Program) via deposit_sol.
  2. Computes protocol fee: fee = amount * (multiplier / divider)
    • Default: 25 / 1000 = 2.5% (see Initialize).
  3. Subtracts fee and moves the net to wsol_vault via transfer_sol using the vault PDA signer.
  4. Syncs WSOL account (sync_wsol) to reflect lamports deposited.
  5. Runs swap_batch:
    • Splits the net into equal parts: swap_amount = amount_net / total_swaps.
    • For each swap i:
      • Reads the account block: vaults, pool, observation, amm_config, buyer’s ATA, and N tick arrays.
      • Verifies/creates buyer’s ATA for output token (get_or_create_ata).
      • Computes other_amount_threshold and sqrt_price_limit_x64.
      • CPI to raydium::cpi::swap_v2, signing as vault PDA.
  6. Emits SwapsSucceeded { buyer, token: wsol_mint, total_amount, total_fee_recollected }.
  7. Increases global_state.total_protocol_fees.

Relevant errors:

  • MissingATA: The passed output_token_account doesn’t match buyer’s ATA for output_mint (the program creates it only if you pass that exact address).
  • InsufficientFunds: In withdraw_fees if you try to withdraw more than accumulated.

6) Minimal TypeScript example (frontend)

Prereqs:

  • Node 18+, Anchor CLI, Solana CLI, program deployed on devnet with the programId in programs/randombag/src/lib.rs.
  • Use Anchor provider or a devnet Connection.

Example (inspired by tests/randombag.test.ts):

import * as anchor from "@coral-xyz/anchor";
import { PublicKey } from "@solana/web3.js";
import { TOKEN_PROGRAM_ID } from "@coral-xyz/anchor/dist/cjs/utils/token";
import { Program } from "@coral-xyz/anchor";
import { Randombag } from "../target/types/randombag";
import { generateRaydiumAccountsPerTokens } from "../tests/utils/raydium";
import { PUBKEYS } from "../tests/utils/pubkeys";

(async () => {
  const provider = anchor.AnchorProvider.env();
  anchor.setProvider(provider);
  const program = anchor.workspace.randombag as Program<Randombag>;

  const WSOL_MINT = PUBKEYS.TOKENS.WSOL_DEV;
  const USDC_MINT = PUBKEYS.TOKENS.USDC_DEV; // target token

  const amount = new anchor.BN(1_000_000); // 0.001 SOL (lamports)
  const totalSwaps = 1;

  // Build remaining accounts + price
  const { remainingAccounts, priceScaled, tickArrayAmount } =
    await generateRaydiumAccountsPerTokens(provider, amount, WSOL_MINT, USDC_MINT);

  const pricesScaled = [priceScaled];
  const tickArrayAmounts = Buffer.from([tickArrayAmount]);

  const ix = await program.methods
    .buyRandomBag(amount, totalSwaps, pricesScaled, tickArrayAmounts)
    .accounts({
      wsolMint: WSOL_MINT,
      tokenProgram: TOKEN_PROGRAM_ID,
    })
    .remainingAccounts(remainingAccounts)
    .instruction();

  const tx = new anchor.web3.Transaction().add(ix);
  const sig = await anchor.web3.sendAndConfirmTransaction(
    provider.connection,
    tx,
    [(provider.wallet as any).payer],
    { skipPreflight: true }
  );

  console.log("buy_random_bag tx:", sig);
})();

Notes:

  • remainingAccounts must contain, for each swap, the 7 base accounts + N tick arrays.
  • output_token_account in the block must be buyer’s ATA for output_mint (the program creates it if it doesn’t exist, but only if you pass that exact address). The helper uses getAssociatedTokenAddressSync.
  • For multiple swaps, concatenate the blocks for each swap and provide total_swaps, prices_scaled, and tick_array_amount with matching lengths.

6.1 Example with ALT (Address Lookup Table) for multiple swaps

When you add many swaps (and thus many remaining accounts), the transaction may exceed size limits. Use a v0 transaction with Address Lookup Tables (ALT) to reference many addresses without blowing the size limit.

Example based on the multi-swap test:

import * as anchor from "@coral-xyz/anchor";
import { ComputeBudgetProgram } from "@solana/web3.js";
import { TOKEN_PROGRAM_ID } from "@coral-xyz/anchor/dist/cjs/utils/token";
import { Program } from "@coral-xyz/anchor";
import { Randombag } from "../target/types/randombag";
import { generateRaydiumAccountsPerTokens } from "../tests/utils/raydium";
import { PUBKEYS } from "../tests/utils/pubkeys";

(async () => {
  const provider = anchor.AnchorProvider.env();
  anchor.setProvider(provider);
  const { connection, wallet } = provider;
  const program = anchor.workspace.randombag as Program<Randombag>;

  const WSOL_MINT = PUBKEYS.TOKENS.WSOL_DEV;

  // Suppose 3 swaps to different tokens
  const amountTotal = new anchor.BN(3_000_000); // 0.003 SOL total
  const totalSwaps = 3;

  // Generate blocks per target
  const targets = [
    PUBKEYS.TOKENS.USDC_DEV,
    PUBKEYS.TOKENS.ANOTHER_TOKEN_DEV,
    PUBKEYS.TOKENS.ANOTHER_TOKEN_2_DEV,
  ];
  const chunks = await Promise.all(
    targets.map((mint) =>
      generateRaydiumAccountsPerTokens(
        provider,
        amountTotal.div(new anchor.BN(totalSwaps)),
        WSOL_MINT,
        mint
      )
    )
  );

  // Concatenate remaining accounts and assemble price/tick arrays vectors
  const remainingAccounts = chunks.flatMap((c) => c.remainingAccounts);
  const pricesScaled = chunks.map((c) => c.priceScaled);
  const tickArrayAmounts = Buffer.from(chunks.map((c) => c.tickArrayAmount));

  // Create a LUT and extend it with required keys
  const slot = await connection.getSlot();
  const [createIx, lookupTableAddress] = anchor.web3.AddressLookupTableProgram.createLookupTable({
    authority: wallet.publicKey,
    payer: wallet.publicKey,
    recentSlot: slot,
  });

  // Extend LUT with the required pubkeys
  const keysToAdd = Array.from(new Set(remainingAccounts.map((a) => a.pubkey.toBase58()))).map(
    (s) => new anchor.web3.PublicKey(s)
  );

  const extendIx = anchor.web3.AddressLookupTableProgram.extendLookupTable({
    payer: wallet.publicKey,
    authority: wallet.publicKey,
    lookupTable: lookupTableAddress,
    addresses: keysToAdd,
  });

  // Send tx to create+extend the LUT
  const prepTx = new anchor.web3.Transaction().add(createIx, extendIx);
  await anchor.web3.sendAndConfirmTransaction(connection, prepTx, [wallet.payer]);

  // Wait until LUT is available and load it
  const { value: lookupTableAccount } = await connection.getAddressLookupTable(lookupTableAddress);
  if (!lookupTableAccount) throw new Error("LUT not found (yet)");

  // Build the buy ix as usual
  const ix = await program.methods
    .buyRandomBag(amountTotal, totalSwaps, pricesScaled, tickArrayAmounts)
    .accounts({ wsolMint: WSOL_MINT, tokenProgram: TOKEN_PROGRAM_ID })
    .remainingAccounts(remainingAccounts)
    .instruction();

  // Optional: request more compute units and heap if needed
  const computeIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 1_400_000 });
  const heapIx = ComputeBudgetProgram.requestHeapFrame({ bytes: 512 * 1024 });

  // Compile to v0 message referencing the LUT
  const messageV0 = new anchor.web3.TransactionMessage({
    payerKey: wallet.publicKey,
    recentBlockhash: (await connection.getLatestBlockhash()).blockhash,
    instructions: [heapIx, computeIx, ix],
  }).compileToV0Message([lookupTableAccount]);

  const txV0 = new anchor.web3.VersionedTransaction(messageV0);
  txV0.sign([wallet.payer]);
  const sig = await connection.sendTransaction(txV0, { skipPreflight: true });
  console.log("buy_random_bag (v0 + ALT) tx:", sig);
})();

Tips:

  • If remaining accounts exceed a single LUT capacity, create multiple LUTs and pass them in compileToV0Message([lut1, lut2, ...]) (as in the test example).
  • LUT creation requires its own confirmed transaction; in production, create and reuse LUTs ahead of time.

7) How to run on devnet

  • Requirements: Node 18+, stable Rust, Anchor CLI, Solana CLI.
  • Useful scripts (package.json):
    • npm run devnet → build+deploy on devnet.
    • npm run test:devnet → run tests without redeploy/rebuild.

Typical steps:

# Install test deps
npm i

# (optional) Build+deploy to devnet
npm run devnet

# Run tests (use devnet)
npm run test:devnet

The main test is tests/randombag.test.ts. Check the "Can buy a random bag!" case (skipped in repo) for a minimal flow, and the multi-swap example (with ALT and compute budget) for heavier scenarios.

8) Fees and fee withdrawal

  • Protocol fee: 2.5% per buy (configurable in initialize).
  • Accumulates in global_state.total_protocol_fees.
  • Withdrawal: withdraw_fees(amount) by global_state.authority.

9) Constants and seeds (reference)

  • Seeds:
    • SEED_GLOBAL_STATE = b"global_state"
    • SEED_VAULT = b"connected"
  • Internal slippage: SLIPPAGE_BPS = 200 (2%).
  • Price scale: PRICE_SCALE = 12.
  • Events:
    • SwapsSucceeded { buyer, token, total_amount, total_fee_recollected }

10) Common pitfalls and tips

  • "Associated token account not found": ensure output_token_account matches buyer’s ATA for output_mint. The program only creates the ATA if you pass that exact address.
  • Out-of-sync IDL: rely on target/types/randombag.ts or anchor.workspace. The IDL in idls/ may be out of date.
  • Missing pool or no liquidity: the generateRaydiumAccountsPerTokens utility uses Raydium’s API to find the pool. If there’s no pool (or it’s empty), the swap will fail.
  • Multiple swaps: mind the order of account blocks and ensure prices_scaled.length == total_swaps and tick_array_amount.length == total_swaps.
  • Compute Budget/ALT: for many swaps, consider adding ComputeBudgetProgram instructions and using Address Lookup Tables like in the example.