@solsdk/liquidity_sdk
Version:
Orca Whirlpool clmm library for automated position management
558 lines (445 loc) • 17.8 kB
Markdown
# @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
```bash
npm install @solsdk/liquidity_sdk
# or
yarn add @solsdk/liquidity_sdk
# or
bun add @solsdk/liquidity_sdk
```
## Quick Start
### Fetching Positions
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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 positions
- **`getDetailedPositions(walletAddress, rpc, pools?, rangeChoices?, funder?)`** - Fetch positions with detailed analysis
- **`openPosition(params)`** - Open a new liquidity position
- **`openPositionWithBaseToken(params)`** - Open position using a base token with automatic swapping
- **`closePositionAndHarvestYield(rpc, wallet, position)`** - Close position and harvest fees
- **`closePositionGracefully(rpc, wallet, position, maxRetries?)`** - Close position with error handling
#### Pool Information
- **`fetchOrcaPools()`** - Fetch all available Orca pools
- **`fetchOrcaPoolByAddress(address)`** - Fetch specific pool by address
- **`getOnChainPool(whirlpool, rpc)`** - Get on-chain pool data with current price
- **`getLiquidityInTicks(params)`** - Get liquidity distribution across ticks
#### Token Operations
- **`fetchTokensWithPrices(walletAddress, rpcUrl?)`** - Get tokens with USD prices
- **`fetchTokensWithBalanceByWallet(walletAddress, rpcUrl?)`** - Get all token balances
- **`fetchNonZeroTokenBalances(walletAddress, rpcUrl?)`** - Get non-zero token balances with prices
- **`getUSDPrice({ mintAddress })`** - Get USD price for a token
#### Utility Functions
- **`convertRawToDecimal(rawAmount, decimals)`** - Convert raw token amount to decimal
- **`convertDecimalToRaw(decimalAmount, decimals)`** - Convert decimal amount to raw
- **`analyzePositionBalance(position)`** - Analyze position relative to price range
- **`divergenceLoss(p, p_i, p_a, p_b, depositA, depositB)`** - Calculate impermanent loss
- **`getEstimatedYield(params)`** - Calculate estimated position yield
### Types and Interfaces
#### Position Types
```typescript
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
```typescript
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
```typescript
// 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
```typescript
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
```typescript
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
```typescript
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
```typescript
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:
```typescript
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
```typescript
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!