UNPKG

@solsdk/liquidity_sdk

Version:

Orca Whirlpool clmm library for automated position management

558 lines (445 loc) 17.8 kB
# @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!