@solsonar/solana-clmm-helpers
v0.1.0
Published
Synchronous tick array PDA derivation, multi-tick swap math, and helpers for Solana concentrated liquidity DEXes (Orca Whirlpool, Raydium CLMM).
Downloads
36
Maintainers
Readme
@solsonar/solana-clmm-helpers
Synchronous, zero-RPC tick array PDA derivation for Solana concentrated liquidity DEXes: Orca Whirlpool and Raydium CLMM.
import { whirlpool, raydiumClmm } from '@solsonar/solana-clmm-helpers';
import { PublicKey } from '@solana/web3.js';
// Derive 3 forward + 3 reverse tick arrays for a Whirlpool from its current state
const whirlpoolPool = new PublicKey('Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE');
const arrays = whirlpool.deriveTickArrays(whirlpoolPool, /* tickCurrent */ -24812, /* tickSpacing */ 4);
// → [{ startTick: -24992, address: PublicKey }, ..., { startTick: -23936, address: PublicKey }]
// Same for Raydium CLMM (different program, different PDA encoding)
const clmmPool = new PublicKey('3UwfrdLTpAjxTRni1boc5HUWe6hzc4HgE5yLdvEp2Noc');
const clmmArrays = raydiumClmm.deriveTickArrays(clmmPool, /* tickCurrent */ -96869, /* tickSpacing */ 10);
// → [{ startTick: -97200, address: PublicKey }, ..., { startTick: -95400, address: PublicKey }]What this package does
Three pure functions per DEX (Whirlpool and Raydium CLMM), no I/O, no SDK runtime calls:
getStartTick(currTick, tickSpacing)— computes the start tick index of the array containingcurrTick. Rounds toward-Infinityfor negative ticks (matches on-chain convention).getTickArrayAddress(pool, startTickIndex, programId?)— derives the PDA for a single tick array. Encodings differ per DEX:- Whirlpool seeds:
["tick_array", pool, start_tick.toString()] - Raydium CLMM seeds:
["tick_array", pool, i32_BE(start_tick)](big-endian, not little-endian)
- Whirlpool seeds:
deriveTickArrays(pool, currTick, tickSpacing, opts?)— returns 3 forward + 3 reverse tick array addresses (configurable viaopts.forwardCount/opts.reverseCount). Filters out indices outside[MIN_TICK_INDEX, MAX_TICK_INDEX].
What this package does not do
- No RPC calls. Caller fetches pool state once (via
getAccountInfoor a Yellowstone subscription) and passestick_current+tick_spacingin. - No swap math. This package only derives addresses; pair it with a CLMM quote engine for output amounts.
- No bitmap_extension parsing yet. Phase 1 returns the naive ±N range from current tick — uninitialized arrays in that range simply won't have on-chain accounts and the consumer must handle that. Bitmap-aware skipping is on the roadmap.
Why
Backrun arbitrage bots need tick array account addresses to build a CLMM swap instruction. Yellowstone subscriptions can populate the cache reactively when a pool sees swap traffic, but pools targeted only as the sell leg of an arbitrage may never be seen in shred triggers — leaving the cache empty when the executor needs them. Eager preload via these helpers fills that gap.
The math is small but the encoding is easy to get wrong. Reference Cetipoo's Rust bot uses seed = "pool_tick_array" for the bitmap extension PDA — a different concept that's easy to confuse with regular tick array PDAs (seed = "tick_array"). This package uses the verified seed and encoding for each DEX, validated against real on-chain accounts.
API
Whirlpool
import { whirlpool } from '@solsonar/solana-clmm-helpers';
whirlpool.WHIRLPOOL_PROGRAM_ID // PublicKey
whirlpool.TICK_ARRAY_SIZE // 88
whirlpool.MIN_TICK_INDEX // -443636
whirlpool.MAX_TICK_INDEX // 443636
whirlpool.getStartTick(currTick, tickSpacing) // → number
whirlpool.getTickArrayAddress(pool, startTickIndex, programId?) // → PublicKey
whirlpool.deriveTickArrays(pool, currTick, tickSpacing, {
forwardCount: 3, // default 3
reverseCount: 3, // default 3
programId: ..., // default WHIRLPOOL_PROGRAM_ID
}) // → Array<{ startTick: number, address: PublicKey }>Raydium CLMM
import { raydiumClmm } from '@solsonar/solana-clmm-helpers';
raydiumClmm.RAYDIUM_CLMM_PROGRAM_ID // PublicKey
raydiumClmm.TICK_ARRAY_SIZE // 60
raydiumClmm.MIN_TICK_INDEX // -443636
raydiumClmm.MAX_TICK_INDEX // 443636
// Same shape as Whirlpool
raydiumClmm.getStartTick(currTick, tickSpacing)
raydiumClmm.getTickArrayAddress(pool, startTickIndex, programId?)
raydiumClmm.deriveTickArrays(pool, currTick, tickSpacing, opts?)Verified against mainnet
The PDA derivation is verified against real on-chain accounts at publish time:
- Whirlpool SOL/USDC
Czfq3xZZ...→ 6/6 tick array addresses match - Raydium CLMM BONK/USDC
3UwfrdLT...→ 6/6 tick array addresses match
See test/whirlpool.test.js and test/raydium-clmm.test.js for the full address-by-address assertions.
License
MIT
