@apex_labs/sdk
v0.7.4
Published
TypeScript SDK for Apex CL pools, Classic pools, routing, farms, ApexVault, and contract ABIs on Robinhood Chain.
Readme
Apex SDK
Production TypeScript SDK for Apex CL and Classic pools on Robinhood Chain. It provides canonical chain definitions, explicit deployment-state handling, Apex fee tiers, deterministic pool address helpers, Classic AMM quoting, mixed CL/Classic path encoding, transaction builders, and provenance-tracked contract ABIs.
Supported execution environments are Node.js 22 or newer and modern bundlers. Both ESM and CommonJS package entry points are verified in CI.
Setup
npm ci
npm run verifyInstall the published package:
npm install @apex_labs/sdkChains and deployments
The SDK exports chain definitions for Robinhood Chain mainnet (4663), Robinhood Chain testnet (46630), and local Anvil (31337):
import {
ROBINHOOD_TESTNET_CHAIN_ID,
getApexDeployment,
robinhoodTestnet,
} from "@apex_labs/sdk";
const state = getApexDeployment(ROBINHOOD_TESTNET_CHAIN_ID);
if (!state || state.status === "unavailable") {
// Render a deliberate unavailable state. Never substitute stale addresses.
console.info(state?.message ?? "unsupported chain");
} else {
const apex = state.config;
console.info(robinhoodTestnet.name, apex.clFactory);
}No Apex contract deployment is bundled for Robinhood Chain yet. Address-bearing entries will only be added from a verified deployment manifest. For an ephemeral local deployment, load the generated manifest into an ApexDeploymentConfig instead of committing local addresses:
import type { ApexDeploymentConfig } from "@apex_labs/sdk";
export const apex: ApexDeploymentConfig = {
chainId: 31337,
clFactory: "0x...",
clPoolDeployer: "0x...",
clSwapRouter: "0x...",
clQuoter: "0x...",
clNonfungiblePositionManager: "0x...",
clMasterChef: "0x...",
smartRouter: "0x...",
mixedQuoter: "0x...",
classicFactory: "0x...",
classicPairInitCodeHash: "0x...",
classicRouter: "0x...",
classicChef: "0x...",
weth: "0x...",
};Core exports
import {
CLFeeAmount,
APEX_CL_POOL_INIT_CODE_HASH,
APEX_CLASSIC_PAIR_INIT_CODE_HASH,
apexDeployments,
robinhoodMainnet,
robinhoodTestnet,
getApexDeployment,
requireApexDeployment,
ClassicChef,
ClassicRouter,
CLMasterChef,
SmartRouter,
ApexVault,
getUserStakedApexVaultNFTs,
getCLTickSpacing,
createCLPool,
computeCLPoolAddress,
getCLPoolStatus,
getClassicPoolStatus,
computeClassicPoolAddress,
quoteClassicExactInput,
encodeMixedRouteToPath,
encodeMixedRouteToQuoteParams,
decodeMixedRoutePath,
parseProtocolFeePacked,
clPoolAbi,
clFactoryAbi,
clMasterChefAbi,
nonfungiblePositionManagerAbi,
swapRouterAbi,
smartRouterAbi,
mixedQuoterAbi,
quoterV2Abi,
classicFactoryAbi,
classicPoolAbi,
classicRouterAbi,
Pool,
Route,
Trade,
SwapRouter,
NonfungiblePositionManager,
SwapQuoter,
TickMath,
nearestUsableTick,
encodeSqrtRatioX96,
} from "@apex_labs/sdk";CL pool address
const pool = computeCLPoolAddress({
config: apex,
tokenA,
tokenB,
fee: CLFeeAmount.FEE_0_30,
});CL pool status
Use getCLPoolStatus before deciding whether the UI should show fee unsupported, create pool, initialize pool, or add liquidity. Pass wrapped-native addresses for native pairs; for example, use WETH instead of an ETH placeholder.
const result = await getCLPoolStatus({
client: publicClient,
config: apex,
tokenA: apex.weth,
tokenB: usdt0.address,
fee: CLFeeAmount.FEE_0_30,
});
if (result.status === "UNSUPPORTED_FEE") showUnsupportedFee();
if (result.status === "NOT_CREATED") showCreatePool();
if (result.status === "NOT_INITIALIZED") showInitializePool(result.pool);
if (result.status === "READY") showAddLiquidity(result.pool);CL pool object
const pool = createCLPool({
tokenA,
tokenB,
fee: CLFeeAmount.FEE_0_30,
sqrtRatioX96,
liquidity,
tickCurrent,
ticks,
});createCLPool returns an Apex CL Pool model with Apex fee tiers installed. Position, trade, route, quoter, swap-router, and NFT-position-manager logic should use the classes exported by @apex_labs/sdk.
Periphery ABIs
Use the exact ABIs exported from compiled Apex contracts:
import {
nonfungiblePositionManagerAbi,
swapRouterAbi,
quoterAbi,
quoterV2Abi,
smartRouterAbi,
mixedQuoterAbi,
tickLensAbi,
interfaceMulticallAbi,
clFactoryAbi,
clMasterChefAbi,
clPoolAbi,
classicFactoryAbi,
classicPoolAbi,
classicRouterAbi,
} from "@apex_labs/sdk";Classic pair address
const stablePair = computeClassicPoolAddress({
config: apex,
tokenA: tokenA.address,
tokenB: tokenB.address,
stable: true,
});Classic quote
const amountOut = quoteClassicExactInput({
amountIn,
tokenIn: tokenA.address,
token0,
token1,
reserve0,
reserve1,
token0Decimals: 18,
token1Decimals: 18,
stable: false,
fee,
});Classic pool status
Use getClassicPoolStatus before deciding whether the UI should show create, first-liquidity, or normal add-liquidity states. Pass wrapped-native addresses for native pairs; for example, use WETH instead of an ETH placeholder.
const result = await getClassicPoolStatus({
client: publicClient,
config: apex,
tokenA: apex.weth,
tokenB: usdt0.address,
stable: false,
});
if (result.status === "NOT_CREATED") showCreateOrAddFirstLiquidity();
if (result.status === "NEEDS_LIQUIDITY") showAddFirstLiquidity(result.pool);
if (result.status === "READY") showAddLiquidity(result.pool);Wallet position discovery
Use these helpers to list wallet-held liquidity positions. They do not include
Classic LP staked in ClassicChef or CL NFTs staked in CLMasterChef.
const positions = await getUserWalletLiquidityPositions({
client: publicClient,
config: apex,
account,
});
positions.classic; // wallet-held Classic pair LP positions
positions.cl; // wallet-held CL NFT positionsIf the UI wants separate reads, call:
await getUserClassicWalletLiquidityPositions({
client: publicClient,
config: apex,
account,
});
await getUserCLWalletLiquidityPositions({
client: publicClient,
config: apex,
account,
});Classic liquidity
Use ClassicRouter for all Classic add/remove liquidity UX. Do not raw-transfer tokens to Classic pairs from the frontend. addLiquidity and addLiquidityETH create the pair if it does not exist, pull both assets atomically, and mint LP in one transaction.
const add = ClassicRouter.addLiquidityCallParameters({
tokenA: apex.weth,
tokenB: usdt0.address,
stable: false,
amountADesired,
amountBDesired,
amountAMin,
amountBMin,
to: account,
deadline,
});
await walletClient.sendTransaction({
to: apex.classicRouter,
data: add.calldata,
value: BigInt(add.value),
});For native ETH deposits, use the ETH helper. The router wraps only the used ETH amount and refunds excess ETH.
const addETH = ClassicRouter.addLiquidityETHCallParameters({
token: usdt0.address,
stable: false,
amountTokenDesired,
amountETHDesired,
amountTokenMin,
amountETHMin,
to: account,
deadline,
});Deployments whose ClassicRouter is wired to ClassicChef also support atomic
add-and-stake helpers. These mint LP to the router and immediately call
ClassicChef.depositFor(pid, liquidity, account), so the user receives a farm
position instead of wallet LP. The pair must already be registered in
ClassicChef; otherwise the transaction reverts.
These selectors require the next ClassicRouter deployment that exposes
classicChef(). Do not call them against an older router address.
const addAndStake = ClassicRouter.addLiquidityAndStakeCallParameters({
tokenA: apex.weth,
tokenB: usdt0.address,
stable: false,
amountADesired,
amountBDesired,
amountAMin,
amountBMin,
to: account,
deadline,
});
const addETHAndStake = ClassicRouter.addLiquidityETHAndStakeCallParameters({
token: usdt0.address,
stable: false,
amountTokenDesired,
amountETHDesired,
amountTokenMin,
amountETHMin,
to: account,
deadline,
});Remove paths are also router-only:
const remove = ClassicRouter.removeLiquidityCallParameters({
tokenA: apex.weth,
tokenB: usdt0.address,
stable: false,
liquidity,
amountAMin,
amountBMin,
to: account,
deadline,
});
const removeETH = ClassicRouter.removeLiquidityETHCallParameters({
token: usdt0.address,
stable: false,
liquidity,
amountTokenMin,
amountETHMin,
to: account,
deadline,
});For LP already staked in ClassicChef, use the Chef helpers. The transaction
target is apex.classicChef, not apex.classicRouter: the Chef must see the
user as msg.sender, withdraw the staked LP to the router, then the router
burns it during the trusted callback. These selectors require the next
ClassicChef/ClassicRouter deployment that exposes withdrawToAndCall and
onClassicChefWithdraw.
const unstakeAndRemove = ClassicChef.withdrawAndRemoveLiquidityCallParameters({
pid,
classicRouter: apex.classicRouter,
tokenA: apex.weth,
tokenB: usdt0.address,
stable: false,
liquidity,
amountAMin,
amountBMin,
to: account,
deadline,
});
await walletClient.sendTransaction({
to: apex.classicChef,
data: unstakeAndRemove.calldata,
value: BigInt(unstakeAndRemove.value),
});
const unstakeAndRemoveETH = ClassicChef.withdrawAndRemoveLiquidityETHCallParameters({
pid,
classicRouter: apex.classicRouter,
token: usdt0.address,
stable: false,
liquidity,
amountTokenMin,
amountETHMin,
to: account,
deadline,
});Classic farm UX
Use ClassicChef for Classic LP staking. Pools are pid-based; read ClassicChef.pidOf(pair) from the exported ABI or an indexed farm list, then encode normal stake/claim/exit calls:
const stakeClassic = ClassicChef.depositCallParameters({
pid,
amount,
});
const stakeClassicFor = ClassicChef.depositForCallParameters({
pid,
amount,
account,
});
const claimClassic = ClassicChef.harvestCallParameters({
pid,
});
const exitClassic = ClassicChef.withdrawCallParameters({
pid,
amount,
});
const exitClassicTo = ClassicChef.withdrawToCallParameters({
pid,
amount,
to: recipient,
});Mixed CL/Classic path
const path = encodeMixedRouteToPath([
{
kind: "CL",
tokenIn: tokenA.address,
tokenOut: tokenB.address,
fee: CLFeeAmount.FEE_0_30,
},
{ kind: "CLASSIC_STABLE", tokenIn: tokenB.address, tokenOut: tokenC.address },
]);Quote that path through MixedQuoter.quoteExactInput(path, amountIn), then execute it with
SmartRouter. The high-level helpers encode the swap, then wrap it in SmartRouter.multicall(deadlineOrPreviousBlockhash, data).
const swap = SmartRouter.mixedExactInputCallParameters(
{ hops, recipient: account, amountIn, amountOutMinimum },
{ deadlineOrPreviousBlockhash: deadline },
);For MixedQuoter.quoteExactInput, encode the route path and Classic-hop flags:
const { path, flags } = encodeMixedRouteToQuoteParams(hops);
const quote = await publicClient.readContract({
address: apex.mixedQuoter,
abi: mixedQuoterAbi,
functionName: "quoteExactInput",
args: [path, flags, amountIn],
});For routes split by protocol:
// Classic volatile section
SmartRouter.classicExactInputCallParameters(
{ amountIn, amountOutMin, path, recipient },
{ deadlineOrPreviousBlockhash: deadline },
);
// Classic stable section
SmartRouter.stableExactInputCallParameters(
{ amountIn, amountOutMin, path, flags, recipient },
{ deadlineOrPreviousBlockhash: deadline },
);
// CL section
SmartRouter.exactInputCallParameters(
{ path, recipient, amountIn, amountOutMinimum },
{ deadlineOrPreviousBlockhash: deadline },
);Use the raw encode* helpers only when deliberately assembling a custom multicall. User-facing swaps should use *CallParameters, or SmartRouter.swapCallParameters(rawCalls, options) for custom batches. These require a deadline or previous blockhash.
When value is nonzero, SmartRouter call-parameter helpers append refundETH() automatically. Token sweeps and WETH unwraps require explicit cleanup entries because the SDK cannot infer the intended token, recipient, or minimum amount for every route.
The mixed periphery is exact-input only. Use the CL-only SwapRouter/QuoterV2 for CL exact-output routes.
CL farm UX
Use CLMasterChef for staked CL positions. It encodes the supported paths without requiring applications to assemble Chef calls directly.
const { calldata, value } = CLMasterChef.mintAndStakeCallParameters(
{
token0: tokenA.address,
token1: tokenB.address,
fee: CLFeeAmount.FEE_0_30,
tickLower,
tickUpper,
amount0Desired,
amount1Desired,
amount0Min,
amount1Min,
account,
deadline,
},
{
stakingReceiver: apex.clMasterChef,
nonfungiblePositionManager: apex.clNonfungiblePositionManager,
},
);mintAndStakeCallParameters takes account and encodes it as the staked NFT owner. It rejects the Chef and NFP manager as owners so callers cannot strand positions in protocol contracts.
When minting with native ETH, pass value in the options object. The SDK appends refundETH() in the same NFP multicall only when value is nonzero.
For fee collection on staked NFTs, prefer collectCallParameters or decreaseAndCollectCallParameters:
const collect = CLMasterChef.collectCallParameters({
tokenId,
recipient: account,
});
const exitPart = CLMasterChef.decreaseAndCollectCallParameters(
{ tokenId, liquidity, amount0Min, amount1Min, deadline },
{ tokenId, recipient: account },
);These helpers call CLMasterChef.multicall(...) and use collectTo with the inner collect recipient set to address(0), so Chef receives the tokens first and immediately sweeps/unwraps them to the final recipient.
For a full exit, use closeAndBurnCallParameters:
const close = CLMasterChef.closeAndBurnCallParameters(
{ tokenId, liquidity, amount0Min, amount1Min, deadline },
{ tokenId, recipient: account },
);This encodes harvest -> decreaseLiquidity -> collectTo -> burn. The harvest happens before liquidity is removed, so it is safe even when rewards are zero and keeps burn compatible with strict empty-position semantics.
ApexVault UX
Use ApexVault for user-facing VeApexToken actions. Users choose reward-token percentages and a compound percentage. Compound is not user-claimable APEX; the user config selects the compound bucket, and the protocol operator later calls the controller and ApexVault compound functions.
const approveVault = ApexVault.encodeVeApexTokenApprove({
tokenId,
spender: apexVault,
});
const stake = ApexVault.stakeCallParameters(tokenId, {
rewardConfigs: [
{ rewardToken: weth, bps: 5_000 },
{ rewardToken: usdc, bps: 2_000 },
],
compoundBps: 3_000,
});The helper rejects configs that do not sum to exactly 10_000 bps, repeat a reward token, use zero bps for a reward token, or use the zero address as a reward token.
To list a user's current staked veNFTs, call the SDK helper:
const tokenIds = await getUserStakedApexVaultNFTs({
client: publicClient,
vault: apex.apexVault,
owner: account,
});Pass activeOnly: true if the UI should hide vault positions that are still custodied but no longer active for rewards, such as positions flushed after becoming ineligible.
This is a current-state read and does not parse logs. Because ApexVault currently exposes positionOwner(tokenId) but not a direct tokensOfOwner(owner) view, the helper scans VeApexToken.tokenOfOwnerByIndex(vault, index) across the vault's custodied veNFTs and filters each token ID with ApexVault.
For an existing staked veNFT:
const updateConfig = ApexVault.setUserConfigCallParameters(tokenId, {
rewardConfigs: [{ rewardToken: weth, bps: 7_000 }],
compoundBps: 3_000,
});
const claim = ApexVault.claimCallParameters({
tokenId,
rewardTokens: [weth, usdc],
});
const extend = ApexVault.extendLockCallParameters({
tokenId,
lockDuration: 26n * 7n * 86_400n,
});
const increase = ApexVault.increaseAmountCallParameters({
tokenId,
amount,
});ApexVault is custodial while a veNFT is staked. For staked veNFTs, route amount increases through ApexVault.increaseAmountCallParameters(...), not raw VeApexToken.depositFor(...), so ApexVault reward weights are refreshed. A direct VeApexToken transfer outside ApexVault is a normal NFT transfer and will not create vault position accounting; treat ApexVault stake and unstake as the canonical earning path.
ABI refresh
After Solidity changes, build the canonical contracts repository and pass its explicit Forge artifact directory and Git commit to the SDK. The sync command never traverses to a parent repository or guesses an artifact source:
forge build
npm run sync:abis -- \
--artifacts-dir /absolute/path/to/apex-contracts/out \
--source-commit "$(git -C /absolute/path/to/apex-contracts rev-parse HEAD)" \
--repository giga-dex/apex-contracts
npm run verifyUse the same command with --check in an artifact-integration job to detect ABI drift without writing. src/abi/provenance.json records the exact ABI hashes and contract source used for each SDK release.
Notes
- Apex periphery exports include
NonfungiblePositionManager,SwapRouter,SmartRouter,Quoter,QuoterV2,MixedQuoter,TickLens, andInterfaceMulticall. MixedQuotersupportsquoteExactInput(bytes,uint256)andquoteExactInput(bytes,uint256[],uint256)for Classic+CL route discovery.- Apex CL pool addresses use the Apex
CLPoolDeployer, not the factory, matching the contract saltkeccak256(abi.encode(token0, token1, fee)). - Apex Classic pair addresses use the Classic factory salt
keccak256(abi.encodePacked(token0, token1, stable)). APEX_CL_POOL_INIT_CODE_HASHandAPEX_CLASSIC_PAIR_INIT_CODE_HASHare exported and used by default; override them only if the compiled contract bytecode changes.- Apex Classic fees are denominated by
1_000_000. - Apex CL protocol fees are denominated by
10_000, so10_000means 100%. - Classic protocol fees are first realized by
collectClassicProtocolFees(pair), which burns FeeCenter-held fee LP into raw pair tokens.pushAndSplitFees(tokens)only splits raw token balances that are already in FeeCenter.
