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
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.
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.jsonwith different args, ignore it. Use the above signature (the one used by tests andtarget/types).
- buyer: Signer (writable) — Your wallet; pays SOL and signs.
- global_state (PDA) — Protocol global state.
- seeds:
b"global_state"
- seeds:
- vault (PDA) — SystemAccount vault receiving SOL from buyer.
- seeds:
b"connected"
- seeds:
- wsol_vault (PDA, TokenAccount) — WSOL SPL account owned by
vault.- seeds:
[b"connected", wsol_mint] - token::authority =
vault
- seeds:
- 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).
For each swap, swap_batch expects a block of accounts in this order:
- input_vault (mut) — Pool vault for input token (WSOL).
- output_vault (mut) — Pool vault for output token.
- output_mint — Output token mint.
- pool_state (mut) — CLMM pool account.
- observation_state (mut) — Pool oracle observation account.
- amm_config — AMM config (Raydium PDA).
- output_token_account (mut) — Buyer's ATA for the output token.
- …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.ts→generateRaydiumAccountsPerTokens(...)- 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 }.
- Resolves the pool with Raydium SDK (devnet), computes pool accounts,
prices_scaled[i]is the pool price scaled by 10^12 (seePRICE_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%).
- Uses
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.
Inside process_buy_random_bag:
- Transfers
amountSOL frombuyertovault(System Program) viadeposit_sol. - Computes protocol fee:
fee = amount * (multiplier / divider)- Default:
25 / 1000= 2.5% (seeInitialize).
- Default:
- Subtracts fee and moves the net to
wsol_vaultviatransfer_solusing the vault PDA signer. - Syncs WSOL account (
sync_wsol) to reflect lamports deposited. - 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_thresholdandsqrt_price_limit_x64. - CPI to
raydium::cpi::swap_v2, signing as vault PDA.
- Splits the net into equal parts:
- Emits
SwapsSucceeded { buyer, token: wsol_mint, total_amount, total_fee_recollected }. - Increases
global_state.total_protocol_fees.
Relevant errors:
MissingATA: The passedoutput_token_accountdoesn’t match buyer’s ATA foroutput_mint(the program creates it only if you pass that exact address).InsufficientFunds: Inwithdraw_feesif you try to withdraw more than accumulated.
Prereqs:
- Node 18+, Anchor CLI, Solana CLI, program deployed on devnet with the
programIdinprograms/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:
remainingAccountsmust contain, for each swap, the 7 base accounts + N tick arrays.output_token_accountin the block must be buyer’s ATA foroutput_mint(the program creates it if it doesn’t exist, but only if you pass that exact address). The helper usesgetAssociatedTokenAddressSync.- For multiple swaps, concatenate the blocks for each swap and provide
total_swaps,prices_scaled, andtick_array_amountwith matching lengths.
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.
- 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:devnetThe 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.
- Protocol fee: 2.5% per buy (configurable in
initialize). - Accumulates in
global_state.total_protocol_fees. - Withdrawal:
withdraw_fees(amount)byglobal_state.authority.
- 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 }
- "Associated token account not found": ensure
output_token_accountmatches buyer’s ATA foroutput_mint. The program only creates the ATA if you pass that exact address. - Out-of-sync IDL: rely on
target/types/randombag.tsoranchor.workspace. The IDL inidls/may be out of date. - Missing pool or no liquidity: the
generateRaydiumAccountsPerTokensutility 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_swapsandtick_array_amount.length == total_swaps. - Compute Budget/ALT: for many swaps, consider adding
ComputeBudgetPrograminstructions and using Address Lookup Tables like in the example.