# @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!
