@solsdk/liquidity_sdk
v1.2.5
Published
Orca Whirlpool clmm library for automated position management
Maintainers
Readme
@solsdk/liquidity_sdk
A robust TypeScript SDK for managing and analyzing concentrated liquidity positions on Solana's Orca Whirlpools. Designed for reliability, performance, and developer experience.
Features
- Effortless retrieval and analysis of concentrated liquidity positions for any wallet
- Accurate calculation of position balances and price ranges
- Seamless conversion between tick indices and prices
- Real-time fee quote fetching and human-readable formatting
- Comprehensive position status analysis (in-range, out-of-range)
- Automated position management with rebalancing capabilities
- Token swapping and portfolio management utilities
- Built with strict TypeScript types and best practices
Installation
npm install @solsdk/liquidity_sdk
# or
yarn add @solsdk/liquidity_sdk
# or
bun add @solsdk/liquidity_sdkQuick Start
Fetching Positions
import { getOrcaPositions } from "@solsdk/liquidity_sdk";
import { createSolanaRpc, mainnet } from "@solana/kit";
// Initialize RPC connection
const rpc = createSolanaRpc(mainnet("https://api.mainnet-beta.solana.com"));
// Specify wallet address
const walletAddress = "YOUR_WALLET_ADDRESS";
// Fetch all positions for the wallet
const positions = await getOrcaPositions(walletAddress, rpc);
for (const position of positions) {
console.log(`Position: ${position.name}`);
console.log(`Current Price: ${position.currentMarketPrice}`);
console.log(`In Range: ${position.isInRange}`);
console.log(`Lower Price: ${position.lowerPrice}`);
console.log(`Upper Price: ${position.upperPrice}`);
}Fetching Detailed Positions with Analysis
import { getDetailedPositions } from "@solsdk/liquidity_sdk";
import { createSolanaRpc, mainnet } from "@solana/kit";
const rpc = createSolanaRpc(mainnet("https://api.mainnet-beta.solana.com"));
const walletAddress = "YOUR_WALLET_ADDRESS";
// Fetch detailed positions with additional analysis
const detailedPositions = await getDetailedPositions(walletAddress, rpc);
for (const position of detailedPositions) {
console.log(`Position: ${position.name}`);
console.log(`Token A Amount: ${position.tokenAAmount}`);
console.log(`Token B Amount: ${position.tokenBAmount}`);
console.log(`Position Value USD: $${position.positionValueUSD.est}`);
console.log(`Total Fees USD: $${position.totalFeesUSD}`);
console.log(`Relative Position: ${position.relativePosition}`);
console.log(`Range: ${position.range}`);
}Opening a New Position
import {
openPosition,
fetchOrcaPoolByAddress,
getOnChainPool,
setWhirlpoolsConfig,
setDefaultFunder
} from "@solsdk/liquidity_sdk";
import {
createKeyPairSignerFromBytes,
createSolanaRpc,
mainnet,
address
} from "@solana/kit";
import * as dotenv from "dotenv";
dotenv.config();
async function openNewPosition() {
// Set up Orca Whirlpools configuration
await setWhirlpoolsConfig("solanaMainnet");
const bytes = await loadKeypairFromFile("./path/to/keypair.json");
const wallet = await createKeyPairSignerFromBytes(bytes);
const rpcUrl = process.env.RPC_URL || "https://api.mainnet-beta.solana.com";
const rpc = createSolanaRpc(mainnet(rpcUrl));
// Set default funder for transactions
setDefaultFunder(wallet);
const whirlpoolAddress = address("POOL_ADDRESS");
const pool = await fetchOrcaPoolByAddress(whirlpoolAddress);
const onChainPool = await getOnChainPool(pool, rpc);
const result = await openPosition({
rpc,
whirlpoolAddress,
params: { tokenA: 1000000n }, // Amount in smallest units
price: onChainPool.price,
lowerMultiple: 0.9,
upperMultiple: 1.1,
slippageToleranceBps: 100,
wallet,
});
console.log(`Position created with mint: ${result.positionMint}`);
console.log(`Transaction signature: ${result.signature}`);
}
openNewPosition();Opening Position with Base Token
import {
openPositionWithBaseToken,
fetchNonZeroTokenBalances,
SOL_MINT_ADDRESS,
USDC_MINT_ADDRESS
} from "@solsdk/liquidity_sdk";
import { createSolanaRpc, mainnet, address } from "@solana/kit";
const rpc = createSolanaRpc(mainnet("https://api.mainnet-beta.solana.com"));
const wallet = await createKeyPairSignerFromBytes(bytes);
const whirlpoolAddress = address("POOL_ADDRESS");
// Get wallet balances
const balances = await fetchNonZeroTokenBalances(wallet.address);
const usdcBalance = balances.find(b => b.address === USDC_MINT_ADDRESS);
if (usdcBalance) {
const result = await openPositionWithBaseToken({
rpc,
whirlpoolAddress,
wallet,
walletByteArray: bytes,
baseTokenAddress: address(USDC_MINT_ADDRESS),
baseTokenAmount: usdcBalance.balance.uiAmount,
lowerMultiple: 0.95, // 5% below current price
upperMultiple: 1.05, // 5% above current price
maxGasUSD: 10,
maxPriceImpact: 0.01 // 1% max slippage
});
console.log(`Position opened: ${result.positionMint}`);
console.log(`Swap loss: ${result.swapLoss}`);
}Closing Positions
import { closePositionAndHarvestYield, closePositionGracefully } from "@solsdk/liquidity_sdk";
// Close a specific position
const closeResult = await closePositionAndHarvestYield(rpc, wallet, position);
console.log(`Position closed: ${closeResult.signature}`);
console.log(`Fee USD: ${closeResult.details.feeUSD}`);
// Close position with error handling and retries
const gracefulResult = await closePositionGracefully(rpc, wallet, position, 3);
if (gracefulResult) {
console.log(`Position closed gracefully: ${gracefulResult.signature}`);
}Token Management
import {
fetchTokensWithPrices,
fetchTokensWithBalanceByWallet,
getUSDPrice
} from "@solsdk/liquidity_sdk";
// Fetch tokens with USD prices
const tokensWithPrices = await fetchTokensWithPrices(walletAddress);
const totalValue = tokensWithPrices.reduce((sum, token) => sum + token.usdValue, 0);
console.log(`Total portfolio value: $${totalValue.toFixed(2)}`);
// Get USD price for a specific token
const solPrice = await getUSDPrice({ mintAddress: SOL_MINT_ADDRESS });
console.log(`SOL price: $${solPrice}`);
// Fetch all token balances
const allBalances = await fetchTokensWithBalanceByWallet(walletAddress);
for (const token of allBalances) {
console.log(`${token.symbol}: ${token.balance.uiAmountString}`);
}API Reference
Core Functions
Position Management
getOrcaPositions(walletAddress, rpc, pools?, funder?)- Fetch basic Orca positionsgetDetailedPositions(walletAddress, rpc, pools?, rangeChoices?, funder?)- Fetch positions with detailed analysisopenPosition(params)- Open a new liquidity positionopenPositionWithBaseToken(params)- Open position using a base token with automatic swappingclosePositionAndHarvestYield(rpc, wallet, position)- Close position and harvest feesclosePositionGracefully(rpc, wallet, position, maxRetries?)- Close position with error handling
Pool Information
fetchOrcaPools()- Fetch all available Orca poolsfetchOrcaPoolByAddress(address)- Fetch specific pool by addressgetOnChainPool(whirlpool, rpc)- Get on-chain pool data with current pricegetLiquidityInTicks(params)- Get liquidity distribution across ticks
Token Operations
fetchTokensWithPrices(walletAddress, rpcUrl?)- Get tokens with USD pricesfetchTokensWithBalanceByWallet(walletAddress, rpcUrl?)- Get all token balancesfetchNonZeroTokenBalances(walletAddress, rpcUrl?)- Get non-zero token balances with pricesgetUSDPrice({ mintAddress })- Get USD price for a token
Utility Functions
convertRawToDecimal(rawAmount, decimals)- Convert raw token amount to decimalconvertDecimalToRaw(decimalAmount, decimals)- Convert decimal amount to rawanalyzePositionBalance(position)- Analyze position relative to price rangedivergenceLoss(p, p_i, p_a, p_b, depositA, depositB)- Calculate impermanent lossgetEstimatedYield(params)- Calculate estimated position yield
Types and Interfaces
Position Types
interface OrcaPosition {
isInRange: boolean;
name: string;
fees: { feeAmountA: number; feeAmountB: number };
address: string;
data: {
liquidity: string;
positionMint: string;
tickLowerIndex: number;
tickUpperIndex: number;
};
tokenA: WhirlpoolToken;
tokenB: WhirlpoolToken;
currentMarketPrice: string;
lowerPrice: number;
upperPrice: number;
whirlpool: {
address: string;
price: string;
tickSpacing: number;
};
}
interface DetailedPosition extends OrcaPosition {
createdAt: Date;
tokenAPrice: number;
tokenBPrice: number;
tokenAAmount: number;
tokenBAmount: number;
positionValueUSD: { min: number; est: number };
totalFeesUSD: number;
relativePosition: number;
range: number;
}Parameter Types
interface OpenPositionParams {
rpc: any;
whirlpoolAddress: Address;
params: IncreaseLiquidityQuoteParam;
price: number;
lowerMultiple: number;
upperMultiple: number;
slippageToleranceBps: number;
wallet: TransactionSigner;
swapDustToAddress?: string;
walletByteArray?: Uint8Array;
maxGasUSD?: number;
}
interface OpenUSDCPositionParams {
rpc: Rpc;
whirlpoolAddress: Address;
wallet: TransactionSigner;
walletByteArray: Uint8Array;
baseTokenAddress: Address;
baseTokenAmount: number;
lowerMultiple: number;
upperMultiple: number;
maxSwapAttempts?: number;
maxGasUSD?: number;
maxPriceImpact?: number;
swapDustToAddress?: Address;
}Constants
// Token addresses
export const SOL_MINT_ADDRESS = "So11111111111111111111111111111111111111112";
export const USDC_MINT_ADDRESS = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
export const PYUSD_MINT_ADDRESS = "2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo";
// Program IDs
export const TOKEN_PROGRAM_ID: Address<"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA">;
export const ASSOCIATED_TOKEN_PROGRAM_ID: Address<"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL">;
// Error codes
export const TOKEN_MAX_EXCEEDED_ERROR: bigint;
export const TOKEN_MIN_SUBCEEDED_ERROR: bigint;
export const INVALID_START_TICK_ERROR: bigint;
export const INSUFFICIENT_FUNDS_ERROR = 1n;Common Use Cases
Automated Position Rebalancing
import {
getDetailedPositions,
closePositionGracefully,
openPositionWithBaseToken,
SOL_MINT_ADDRESS,
USDC_MINT_ADDRESS
} from "@solsdk/liquidity_sdk";
async function rebalancePositions(walletAddress: string, rpc: any, wallet: TransactionSigner) {
const positions = await getDetailedPositions(walletAddress, rpc);
const baseTokens = [SOL_MINT_ADDRESS, USDC_MINT_ADDRESS];
for (const position of positions) {
// Check if position is out of range or at extreme ends
if (!position.isInRange || position.relativePosition <= 0.1 || position.relativePosition >= 0.9) {
console.log(`Rebalancing position: ${position.name}`);
// Determine base token (prefer stablecoins)
const isTokenABase = baseTokens.includes(position.tokenA.address);
const baseToken = isTokenABase ? position.tokenA : position.tokenB;
// Close existing position
const closeResult = await closePositionGracefully(rpc, wallet, position);
if (!closeResult) continue;
// Get updated balances
const balances = await fetchNonZeroTokenBalances(walletAddress);
const baseBalance = balances.find(b => b.address === baseToken.address);
if (baseBalance && baseBalance.balance.uiAmount > 0) {
// Open new position with tighter range
await openPositionWithBaseToken({
rpc,
whirlpoolAddress: address(position.whirlpool.address),
wallet,
walletByteArray: bytes,
baseTokenAddress: address(baseToken.address),
baseTokenAmount: baseBalance.balance.uiAmount,
lowerMultiple: 0.98, // 2% below current price
upperMultiple: 1.02, // 2% above current price
maxGasUSD: 10
});
}
}
}
}Portfolio Analysis
import { getDetailedPositions, fetchTokensWithPrices } from "@solsdk/liquidity_sdk";
async function analyzePortfolio(walletAddress: string, rpc: any) {
// Get all positions
const positions = await getDetailedPositions(walletAddress, rpc);
// Get token balances
const tokens = await fetchTokensWithPrices(walletAddress);
// Calculate total values
const totalPositionValue = positions.reduce((sum, pos) => sum + pos.positionValueUSD.est, 0);
const totalTokenValue = tokens.reduce((sum, token) => sum + token.usdValue, 0);
const totalFees = positions.reduce((sum, pos) => sum + pos.totalFeesUSD, 0);
console.log(`Portfolio Analysis:`);
console.log(`Total Position Value: $${totalPositionValue.toFixed(2)}`);
console.log(`Total Token Value: $${totalTokenValue.toFixed(2)}`);
console.log(`Total Portfolio Value: $${(totalPositionValue + totalTokenValue).toFixed(2)}`);
console.log(`Total Fees Earned: $${totalFees.toFixed(2)}`);
// Analyze position health
const inRangePositions = positions.filter(p => p.isInRange);
const outOfRangePositions = positions.filter(p => !p.isInRange);
console.log(`\nPosition Health:`);
console.log(`In Range: ${inRangePositions.length}/${positions.length}`);
console.log(`Out of Range: ${outOfRangePositions.length}/${positions.length}`);
// Show position details
for (const position of positions) {
console.log(`\n${position.name}:`);
console.log(` Value: $${position.positionValueUSD.est.toFixed(2)}`);
console.log(` In Range: ${position.isInRange}`);
console.log(` Relative Position: ${(position.relativePosition * 100).toFixed(1)}%`);
console.log(` Range Width: ${(position.range * 100).toFixed(1)}%`);
console.log(` Fees: $${position.totalFeesUSD.toFixed(2)}`);
}
}Yield Calculation
import { getEstimatedYield, divergenceLoss } from "@solsdk/liquidity_sdk";
// Calculate estimated yield for a position
const yieldEstimate = await getEstimatedYield({
poolAddress: "POOL_ADDRESS",
tokenAAmountUSD: 1000,
tokenBAmountUSD: 1000,
range: 0.1, // 10% total range
statsType: "24h"
});
console.log(`Estimated 24h yield: ${yieldEstimate}%`);
// Calculate impermanent loss
const ilResult = divergenceLoss(
1.05, // current price (5% higher)
1.0, // initial price
0.95, // lower bound
1.05, // upper bound
1000, // initial deposit A
1000 // initial deposit B
);
console.log(`Impermanent Loss: ${ilResult.totalIL.toFixed(2)}%`);
console.log(`LP Value: $${ilResult.lpValue.toFixed(2)}`);
console.log(`HODL Value: $${ilResult.holdValue.toFixed(2)}`);Error Handling Best Practices
import { OrcaError, SolanaError } from "@solsdk/liquidity_sdk";
async function safePositionOperation() {
try {
const result = await openPosition(params);
return result;
} catch (error) {
if (error instanceof OrcaError) {
console.error(`Orca Error [${error.code}]: ${error.message}`);
// Handle specific error codes
if (error.code === TOKEN_MAX_EXCEEDED_ERROR) {
console.log("Token amount exceeds maximum, reducing amount...");
// Retry with smaller amount
}
} else if (error instanceof SolanaError) {
console.error(`Solana Error: ${error.message}`);
console.error(`Cause:`, error.cause);
// Handle network or transaction errors
if (error.message.includes("insufficient funds")) {
console.log("Insufficient funds for transaction");
}
} else {
console.error(`Unexpected error:`, error);
}
throw error;
}
}Advanced Features
Liquidity Depth Visualization
The library includes tools for analyzing liquidity distribution:
import { getLiquidityInTicks } from "@solsdk/liquidity_sdk";
const liquidityData = await getLiquidityInTicks({
poolAddress: address("POOL_ADDRESS"),
rpc
});
console.log(`Current price: ${liquidityData.currentPrice}`);
console.log(`Liquidity points: ${liquidityData.data.length}`);
// Export for visualization
const fs = require('fs');
fs.writeFileSync('liquidity_points.json', JSON.stringify(liquidityData.data, null, 2));The exported data can be used with visualization tools to create liquidity depth charts showing bid/ask depth around the current price.
Transaction Simulation
import { simulateTransaction } from "@solsdk/liquidity_sdk";
// Simulate transaction before execution
const simulation = await simulateTransaction(rpc, wallet, instructions);
console.log(`Estimated fee: $${simulation.estimatedFee}`);Why @solsdk/liquidity_sdk?
- Strict Type Safety: 100% TypeScript, no
any, no type assertions, no surprises - Modern API: Clean, minimal, and intuitive interface using latest Solana libraries
- Performance: Optimized for speed and low-latency operations
- Reliability: Built with best practices (KISS, DRY, SOLID) and thoroughly tested
- Developer Experience: Clear error messages, predictable behavior, and comprehensive type hints
- Production Ready: Used in live trading environments with robust error handling
- Comprehensive: Covers the full lifecycle from position analysis to automated management
Requirements
- Node.js 16+ or Bun
- TypeScript 5.0+
- Solana RPC endpoint (mainnet or devnet)
- Wallet keypair for transaction signing
License
MIT
Ready to get started?
Install @solsdk/liquidity_sdk and build your next Solana liquidity management tool with confidence!
