Skip to main content

DLMM API Reference

Complete API reference for the Saros DLMM TypeScript SDK (@saros-finance/dlmm-sdk).

Core Classes

DLMMPool

Main class for interacting with DLMM pools.

Constructor & Loading

class DLMMPool {
static async load(
connection: Connection,
poolAddress: PublicKey
): Promise<DLMMPool>
}

Example:

const pool = await DLMMPool.load(connection, new PublicKey(poolAddress));

Properties

interface DLMMPool {
readonly poolAddress: PublicKey;
readonly tokenX: TokenInfo;
readonly tokenY: TokenInfo;
readonly activeId: number; // Current active bin ID
readonly feeTier: number; // Fee in basis points
readonly binStep: number; // Price step between bins
readonly connection: Connection;
}

Methods

getCurrentPrice()
getCurrentPrice(): number

Returns the current trading price (tokenY per tokenX).

getBinIdFromPrice()
getBinIdFromPrice(price: number): number

Converts a price to the corresponding bin ID.

getPriceFromBinId()
getPriceFromBinId(binId: number): number

Converts a bin ID to its corresponding price.

getBinLiquidity()
async getBinLiquidity(binId: number): Promise<BinLiquidity>

Returns:

interface BinLiquidity {
binId: number;
liquidityX: number;
liquidityY: number;
price: number;
feeX: number;
feeY: number;
}
getActiveBins()
async getActiveBins(range?: number): Promise<BinLiquidity[]>

Returns bins with liquidity around the current active bin.

refresh()
async refresh(): Promise<void>

Updates pool state from blockchain.


LiquidityPosition

Class representing a liquidity position in a DLMM pool.

Creation

async function createPosition(params: PositionParameters): Promise<LiquidityPosition>

interface PositionParameters {
pool: DLMMPool;
lowerBinId: number;
upperBinId: number;
tokenXAmount: number;
tokenYAmount: number;
wallet: PublicKey;
slippageTolerance: number;
}

Properties

interface LiquidityPosition {
readonly positionId: string;
readonly pool: DLMMPool;
readonly lowerBinId: number;
readonly upperBinId: number;
readonly walletAddress: string;
readonly createdAt: Date;
}

Methods

getPriceRange()
getPriceRange(): { lower: number; upper: number }

Returns the position's price range.

getCurrentAmounts()
async getCurrentAmounts(): Promise<{ tokenX: number; tokenY: number }>

Returns current token amounts in the position.

getTotalValue()
async getTotalValue(): Promise<number>

Returns total position value in USD.

getAccumulatedFees()
async getAccumulatedFees(): Promise<{ tokenX: number; tokenY: number; total: number }>

Returns accumulated but uncollected fees.

isActive()
isActive(): boolean

Returns true if current price is within position range.

addLiquidity()
async addLiquidity(params: {
tokenXAmount: number;
tokenYAmount: number;
slippageTolerance: number;
}): Promise<TransactionResult>
removeLiquidity()
async removeLiquidity(
percentage: number
): Promise<{ tokenX: number; tokenY: number }>

Remove liquidity from position (percentage: 0.0 to 1.0).

collectFees()
async collectFees(): Promise<{ tokenX: number; tokenY: number }>

Collect accumulated fees to wallet.

close()
async close(): Promise<{ tokenX: number; tokenY: number }>

Close position completely (remove all liquidity and collect fees).


Utility Functions

Price and Bin Calculations

// Calculate bin price from bin ID
function calculateBinPrice(binId: number, binStep: number): number {
return Math.pow(1 + binStep / 10000, binId);
}

// Find bin ID for target price
function findBinId(targetPrice: number, binStep: number): number {
return Math.floor(Math.log(targetPrice) / Math.log(1 + binStep / 10000));
}

// Calculate number of bins in a price range
function calculateBinCount(
lowerPrice: number,
upperPrice: number,
binStep: number
): number {
const lowerBinId = findBinId(lowerPrice, binStep);
const upperBinId = findBinId(upperPrice, binStep);
return upperBinId - lowerBinId + 1;
}

Liquidity Distribution

// Calculate optimal token distribution for a range
function calculateTokenDistribution(
lowerPrice: number,
upperPrice: number,
currentPrice: number,
totalValueUSD: number
): { tokenX: number; tokenY: number } {

if (currentPrice <= lowerPrice) {
// Price below range - all token Y
return {
tokenX: 0,
tokenY: totalValueUSD
};
}

if (currentPrice >= upperPrice) {
// Price above range - all token X
return {
tokenX: totalValueUSD / currentPrice,
tokenY: 0
};
}

// Price in range - calculate optimal distribution
const sqrt_lower = Math.sqrt(lowerPrice);
const sqrt_current = Math.sqrt(currentPrice);
const sqrt_upper = Math.sqrt(upperPrice);

const deltaL = 1; // Normalized liquidity amount

const tokenY = deltaL * (sqrt_current - sqrt_lower);
const tokenX = deltaL * (1/sqrt_lower - 1/sqrt_upper);

const totalValue = tokenX * currentPrice + tokenY;
const valueRatio = totalValueUSD / totalValue;

return {
tokenX: tokenX * valueRatio,
tokenY: tokenY * valueRatio
};
}

Fee Calculations

// Calculate expected fees for a position
function calculateExpectedFees(
liquidityProvided: number, // Liquidity amount
tradingVolume: number, // Expected trading volume
feeTier: number, // Pool fee tier (basis points)
marketShare: number // Position's share of pool liquidity
): number {
const feeRate = feeTier / 10000; // Convert basis points to decimal
const positionVolume = tradingVolume * marketShare;
return positionVolume * feeRate;
}

// Calculate APR from fees
function calculateFeeAPR(
feesEarnedUSD: number,
positionValueUSD: number,
timeframeDays: number
): number {
const annualizedFees = feesEarnedUSD * (365 / timeframeDays);
return (annualizedFees / positionValueUSD) * 100;
}

// Calculate break-even time for position
function calculateBreakEvenTime(
positionValueUSD: number,
dailyFeeRate: number,
impermanentLossPercent: number
): number {
// Days needed for fees to offset impermanent loss
const impermanentLossUSD = positionValueUSD * (Math.abs(impermanentLossPercent) / 100);
return impermanentLossUSD / dailyFeeRate;
}

Advanced APIs

Batch Operations

// Execute multiple position operations atomically
async function batchPositionOperations(
operations: PositionOperation[],
walletAddress: string
): Promise<BatchResult> {

const results: OperationResult[] = [];
let successCount = 0;

for (const operation of operations) {
try {
let result: any;

switch (operation.type) {
case 'create':
result = await createPosition(operation.params);
break;
case 'add_liquidity':
result = await operation.position.addLiquidity(operation.params);
break;
case 'remove_liquidity':
result = await operation.position.removeLiquidity(operation.params.percentage);
break;
case 'collect_fees':
result = await operation.position.collectFees();
break;
case 'close':
result = await operation.position.close();
break;
default:
throw new Error(`Unknown operation type: ${operation.type}`);
}

results.push({
operation: operation.type,
success: true,
result
});
successCount++;

} catch (error) {
results.push({
operation: operation.type,
success: false,
error: error.message
});
}
}

return {
totalOperations: operations.length,
successful: successCount,
failed: operations.length - successCount,
results
};
}

interface PositionOperation {
type: 'create' | 'add_liquidity' | 'remove_liquidity' | 'collect_fees' | 'close';
position?: LiquidityPosition;
params: any;
}

Position Analytics API

class PositionAnalyticsAPI {
async getPositionMetrics(
positionId: string,
timeframe: '1d' | '7d' | '30d' | '90d' = '7d'
): Promise<DetailedMetrics> {

const position = await LiquidityPosition.load(connection, new PublicKey(positionId));
const days = this.timeframeToDays(timeframe);

return {
basic: {
positionId,
currentValue: await position.getTotalValue(),
feesEarned: await position.getAccumulatedFees(),
isActive: position.isActive(),
ageInDays: position.getAgeInDays()
},
performance: {
feeAPR: await this.calculateFeeAPR(position, days),
impermanentLoss: await this.calculateIL(position),
totalReturn: await this.calculateTotalReturn(position),
sharpeRatio: await this.calculateSharpeRatio(position, days),
maxDrawdown: await this.calculateMaxDrawdown(position, days)
},
utilization: {
timeInRange: await this.calculateTimeInRange(position, days),
capitalEfficiency: await this.calculateCapitalEfficiency(position),
volumeCapture: await this.calculateVolumeCapture(position, days)
},
risk: {
volatilityExposure: await this.calculateVolatilityExposure(position),
concentrationRisk: this.calculateConcentrationRisk(position),
liquidityRisk: await this.calculateLiquidityRisk(position)
}
};
}

private timeframeToDays(timeframe: string): number {
switch (timeframe) {
case '1d': return 1;
case '7d': return 7;
case '30d': return 30;
case '90d': return 90;
default: return 7;
}
}

// Implement calculation methods...
}

interface DetailedMetrics {
basic: {
positionId: string;
currentValue: number;
feesEarned: { tokenX: number; tokenY: number; total: number };
isActive: boolean;
ageInDays: number;
};
performance: {
feeAPR: number;
impermanentLoss: number;
totalReturn: number;
sharpeRatio: number;
maxDrawdown: number;
};
utilization: {
timeInRange: number;
capitalEfficiency: number;
volumeCapture: number;
};
risk: {
volatilityExposure: number;
concentrationRisk: number;
liquidityRisk: number;
};
}

Error Handling

DLMM-Specific Errors

enum DLMMErrorCode {
INVALID_BIN_RANGE = 'INVALID_BIN_RANGE',
INSUFFICIENT_LIQUIDITY = 'INSUFFICIENT_LIQUIDITY',
POSITION_NOT_FOUND = 'POSITION_NOT_FOUND',
BIN_NOT_ACTIVE = 'BIN_NOT_ACTIVE',
PRICE_OUT_OF_RANGE = 'PRICE_OUT_OF_RANGE',
SLIPPAGE_EXCEEDED = 'SLIPPAGE_EXCEEDED',
MINIMUM_LIQUIDITY = 'MINIMUM_LIQUIDITY'
}

class DLMMError extends Error {
constructor(
public code: DLMMErrorCode,
message: string,
public details?: any
) {
super(message);
this.name = 'DLMMError';
}
}

// Error handler utility
function handleDLMMError(error: any): never {
const message = error.message?.toLowerCase() || '';

if (message.includes('invalid bin') || message.includes('invalid range')) {
throw new DLMMError(
DLMMErrorCode.INVALID_BIN_RANGE,
'Invalid bin range specified',
{ originalError: error.message }
);
}

if (message.includes('insufficient liquidity')) {
throw new DLMMError(
DLMMErrorCode.INSUFFICIENT_LIQUIDITY,
'Insufficient liquidity for operation',
{ originalError: error.message }
);
}

if (message.includes('position not found')) {
throw new DLMMError(
DLMMErrorCode.POSITION_NOT_FOUND,
'Liquidity position not found',
{ originalError: error.message }
);
}

// Default error handling
throw new DLMMError(
DLMMErrorCode.PRICE_OUT_OF_RANGE,
`DLMM operation failed: ${error.message}`,
{ originalError: error.message }
);
}

Event Monitoring

Position Event Listeners

interface PositionEvent {
type: 'liquidity_added' | 'liquidity_removed' | 'fees_collected' | 'price_moved';
positionId: string;
timestamp: number;
data: any;
}

class PositionEventListener {
private subscriptions: Map<string, number> = new Map();
private eventHandlers: Map<string, (event: PositionEvent) => void> = new Map();

async subscribeToPosition(
positionId: string,
handler: (event: PositionEvent) => void
): Promise<void> {

// Subscribe to position account changes
const subscriptionId = this.connection.onAccountChange(
new PublicKey(positionId),
(accountInfo, context) => {
const event = this.parsePositionEvent(positionId, accountInfo, context);
if (event) {
handler(event);
}
},
'confirmed'
);

this.subscriptions.set(positionId, subscriptionId);
this.eventHandlers.set(positionId, handler);
}

async subscribeToPoolPrice(
poolAddress: string,
handler: (priceEvent: PriceEvent) => void
): Promise<void> {

const subscriptionId = this.connection.onAccountChange(
new PublicKey(poolAddress),
(accountInfo, context) => {
const priceEvent = this.parsePoolEvent(poolAddress, accountInfo, context);
if (priceEvent) {
handler(priceEvent);
}
},
'confirmed'
);

this.subscriptions.set(`pool_${poolAddress}`, subscriptionId);
}

unsubscribe(identifier: string): void {
const subscriptionId = this.subscriptions.get(identifier);
if (subscriptionId !== undefined) {
this.connection.removeAccountChangeListener(subscriptionId);
this.subscriptions.delete(identifier);
this.eventHandlers.delete(identifier);
}
}

unsubscribeAll(): void {
for (const [identifier] of this.subscriptions) {
this.unsubscribe(identifier);
}
}

private parsePositionEvent(
positionId: string,
accountInfo: AccountInfo<Buffer>,
context: Context
): PositionEvent | null {
// Parse account data to determine what changed
// Implementation depends on position account structure

return {
type: 'liquidity_added', // Determine actual event type
positionId,
timestamp: Date.now(),
data: {
slot: context.slot,
// Additional event data
}
};
}

private parsePoolEvent(
poolAddress: string,
accountInfo: AccountInfo<Buffer>,
context: Context
): PriceEvent | null {
// Parse pool account to extract price changes

return {
type: 'price_changed',
poolAddress,
timestamp: Date.now(),
data: {
newPrice: 0, // Extract from account data
activeId: 0, // Extract active bin ID
slot: context.slot
}
};
}
}

interface PriceEvent {
type: 'price_changed' | 'active_bin_changed';
poolAddress: string;
timestamp: number;
data: {
newPrice?: number;
activeId?: number;
slot: number;
};
}

Testing Utilities

Mock DLMM Environment

// Testing utilities for DLMM positions
export class MockDLMMPool extends DLMMPool {
private mockPrice: number;
private mockActiveBin: number;

constructor(
poolAddress: PublicKey,
tokenX: TokenInfo,
tokenY: TokenInfo,
initialPrice: number
) {
super(); // Initialize base class
this.mockPrice = initialPrice;
this.mockActiveBin = this.getBinIdFromPrice(initialPrice);
}

// Override methods for testing
getCurrentPrice(): number {
return this.mockPrice;
}

setMockPrice(newPrice: number): void {
this.mockPrice = newPrice;
this.mockActiveBin = this.getBinIdFromPrice(newPrice);
}

async simulatePriceMovement(
priceChanges: Array<{ price: number; volumeMultiplier: number }>,
position: LiquidityPosition
): Promise<SimulationResult> {

let totalFees = 0;
let timeInRange = 0;

for (const change of priceChanges) {
this.setMockPrice(change.price);

const range = position.getPriceRange();
const isInRange = change.price >= range.lower && change.price <= range.upper;

if (isInRange) {
timeInRange += 1;

// Simulate fee accumulation
const baseFees = this.calculateBaseFees(position);
totalFees += baseFees * change.volumeMultiplier;
}
}

return {
totalFeesEarned: totalFees,
timeInRangePercent: (timeInRange / priceChanges.length) * 100,
finalPrice: this.mockPrice,
impermanentLoss: this.calculateFinalIL(position, priceChanges[0].price, this.mockPrice)
};
}

private calculateBaseFees(position: LiquidityPosition): number {
// Simulate fee generation based on position characteristics
const liquidityShare = position.getLiquidityShare();
const poolFeeRate = this.feeTier / 10000;
const estimatedVolume = 10000; // Mock daily volume

return (estimatedVolume * poolFeeRate * liquidityShare) / 24; // Hourly fees
}
}

Position Testing Framework

// Test framework for position strategies
export class PositionTester {
private scenarios: TestScenario[] = [];

addScenario(scenario: TestScenario): void {
this.scenarios.push(scenario);
}

async runAllTests(
positionConfig: PositionConfig,
pool: DLMMPool
): Promise<TestResults> {

const results: ScenarioResult[] = [];

for (const scenario of this.scenarios) {
console.log(`Running scenario: ${scenario.name}`);

try {
const result = await this.runScenario(scenario, positionConfig, pool);
results.push({
scenario: scenario.name,
success: true,
metrics: result
});
} catch (error) {
results.push({
scenario: scenario.name,
success: false,
error: error.message
});
}
}

return {
totalScenarios: this.scenarios.length,
passed: results.filter(r => r.success).length,
failed: results.filter(r => !r.success).length,
results
};
}

private async runScenario(
scenario: TestScenario,
config: PositionConfig,
pool: DLMMPool
): Promise<ScenarioMetrics> {

// Create mock position
const mockPool = new MockDLMMPool(
pool.poolAddress,
pool.tokenX,
pool.tokenY,
scenario.initialPrice
);

const position = await this.createMockPosition(config, mockPool);

// Run price simulation
const simulation = await mockPool.simulatePriceMovement(
scenario.priceMovements,
position
);

return {
finalValue: simulation.finalPrice * position.getCurrentAmounts().tokenX +
position.getCurrentAmounts().tokenY,
totalFees: simulation.totalFeesEarned,
maxDrawdown: this.calculateMaxDrawdown(scenario.priceMovements, config),
utilization: simulation.timeInRangePercent,
sharpeRatio: this.calculateSharpeRatio(simulation)
};
}
}

interface TestScenario {
name: string;
description: string;
initialPrice: number;
priceMovements: Array<{ price: number; volumeMultiplier: number }>;
expectedOutcome: 'profit' | 'loss' | 'neutral';
}

interface ScenarioMetrics {
finalValue: number;
totalFees: number;
maxDrawdown: number;
utilization: number;
sharpeRatio: number;
}

Constants and Configuration

Default Configuration

export const DLMM_DEFAULTS = {
SLIPPAGE_TOLERANCE: 0.01, // 1%
MIN_POSITION_SIZE: 100, // $100
MAX_POSITION_SIZE: 1000000, // $1M
DEFAULT_RANGE_PERCENT: 10, // ±10%
FEE_COLLECTION_THRESHOLD: 1, // $1
MAX_GAS_COST_RATIO: 0.1, // 10% of fees

BIN_STEPS: {
STABLE: 1, // 0.01% per bin
LOW: 10, // 0.1% per bin
MEDIUM: 60, // 0.6% per bin
HIGH: 200 // 2% per bin
},

FEE_TIERS: {
STABLE: 1, // 0.01%
LOW: 5, // 0.05%
MEDIUM: 30, // 0.3%
HIGH: 100 // 1%
}
} as const;

Validation Functions

function validatePositionParameters(params: PositionParameters): void {
// Validate bin range
if (params.lowerBinId >= params.upperBinId) {
throw new DLMMError(
DLMMErrorCode.INVALID_BIN_RANGE,
'Lower bin ID must be less than upper bin ID'
);
}

// Validate amounts
if (params.tokenXAmount < 0 || params.tokenYAmount < 0) {
throw new DLMMError(
DLMMErrorCode.INSUFFICIENT_LIQUIDITY,
'Token amounts must be positive'
);
}

// Validate slippage
if (params.slippageTolerance < 0 || params.slippageTolerance > 1) {
throw new Error('Slippage tolerance must be between 0 and 1');
}

// Validate position size
const totalValue = params.tokenXAmount * params.pool.getCurrentPrice() + params.tokenYAmount;
if (totalValue < DLMM_DEFAULTS.MIN_POSITION_SIZE) {
throw new DLMMError(
DLMMErrorCode.MINIMUM_LIQUIDITY,
`Position value $${totalValue} below minimum $${DLMM_DEFAULTS.MIN_POSITION_SIZE}`
);
}
}

function validatePriceRange(
lowerPrice: number,
upperPrice: number,
currentPrice: number
): void {
if (lowerPrice >= upperPrice) {
throw new Error('Lower price must be less than upper price');
}

if (lowerPrice <= 0 || upperPrice <= 0) {
throw new Error('Prices must be positive');
}

// Warn if range is extremely wide or narrow
const rangePercent = ((upperPrice - lowerPrice) / currentPrice) * 100;

if (rangePercent > 200) {
console.warn(`Very wide range detected: ${rangePercent.toFixed(1)}% - consider narrowing for better capital efficiency`);
}

if (rangePercent < 1) {
console.warn(`Very narrow range detected: ${rangePercent.toFixed(2)}% - high rebalancing risk`);
}
}

Performance Optimization

Efficient Data Fetching

class DLMMDataManager {
private poolCache: Map<string, { pool: DLMMPool; timestamp: number }> = new Map();
private positionCache: Map<string, { position: LiquidityPosition; timestamp: number }> = new Map();
private cacheDuration = 30000; // 30 seconds

async getPool(poolAddress: string): Promise<DLMMPool> {
const cached = this.poolCache.get(poolAddress);

if (cached && Date.now() - cached.timestamp < this.cacheDuration) {
return cached.pool;
}

const pool = await DLMMPool.load(this.connection, new PublicKey(poolAddress));
this.poolCache.set(poolAddress, { pool, timestamp: Date.now() });

return pool;
}

async batchLoadPositions(positionIds: string[]): Promise<LiquidityPosition[]> {
const batchSize = 10; // Load in batches to avoid rate limits
const positions: LiquidityPosition[] = [];

for (let i = 0; i < positionIds.length; i += batchSize) {
const batch = positionIds.slice(i, i + batchSize);

const batchPromises = batch.map(async (id) => {
const cached = this.positionCache.get(id);

if (cached && Date.now() - cached.timestamp < this.cacheDuration) {
return cached.position;
}

const position = await LiquidityPosition.load(this.connection, new PublicKey(id));
this.positionCache.set(id, { position, timestamp: Date.now() });

return position;
});

const batchResults = await Promise.all(batchPromises);
positions.push(...batchResults);

// Small delay between batches
if (i + batchSize < positionIds.length) {
await new Promise(resolve => setTimeout(resolve, 100));
}
}

return positions;
}

invalidateCache(type: 'pools' | 'positions' | 'all' = 'all'): void {
switch (type) {
case 'pools':
this.poolCache.clear();
break;
case 'positions':
this.positionCache.clear();
break;
case 'all':
this.poolCache.clear();
this.positionCache.clear();
break;
}
}
}

Integration Examples

React Component Example

// Complete React component for position management
import React, { useState, useEffect } from 'react';
import { LiquidityPosition, DLMMPool } from '@saros-finance/dlmm-sdk';

interface PositionManagerProps {
pool: DLMMPool;
walletAddress: string;
}

export function PositionManager({ pool, walletAddress }: PositionManagerProps) {
const [positions, setPositions] = useState<LiquidityPosition[]>([]);
const [selectedPosition, setSelectedPosition] = useState<LiquidityPosition | null>(null);
const [newRange, setNewRange] = useState({ lower: '', upper: '' });

useEffect(() => {
loadUserPositions();
}, [walletAddress, pool]);

const loadUserPositions = async () => {
try {
const userPositions = await pool.getUserPositions(walletAddress);
setPositions(userPositions);
} catch (error) {
console.error('Failed to load positions:', error);
}
};

const createNewPosition = async () => {
try {
const lowerPrice = parseFloat(newRange.lower);
const upperPrice = parseFloat(newRange.upper);

const position = await createConcentratedPosition(
pool,
lowerPrice,
upperPrice,
1.0, // Token X amount
100, // Token Y amount
walletAddress
);

setPositions([...positions, position]);
setNewRange({ lower: '', upper: '' });

} catch (error) {
alert(`Failed to create position: ${error.message}`);
}
};

const closePosition = async (position: LiquidityPosition) => {
try {
await position.close();
setPositions(positions.filter(p => p.positionId !== position.positionId));
if (selectedPosition?.positionId === position.positionId) {
setSelectedPosition(null);
}
} catch (error) {
alert(`Failed to close position: ${error.message}`);
}
};

return (
<div className="position-manager">
<h2>DLMM Position Manager</h2>

{/* Create New Position */}
<section className="create-position">
<h3>Create New Position</h3>
<div>
<input
type="number"
placeholder="Lower Price"
value={newRange.lower}
onChange={(e) => setNewRange({...newRange, lower: e.target.value})}
/>
<input
type="number"
placeholder="Upper Price"
value={newRange.upper}
onChange={(e) => setNewRange({...newRange, upper: e.target.value})}
/>
<button onClick={createNewPosition}>Create Position</button>
</div>
<p>Current Price: {pool.getCurrentPrice().toFixed(4)}</p>
</section>

{/* Existing Positions */}
<section className="existing-positions">
<h3>Your Positions</h3>
<div className="positions-list">
{positions.map(position => (
<div key={position.positionId} className="position-card">
<h4>Position #{position.positionId.slice(0, 8)}...</h4>
<div className="position-details">
<p>Range: {position.getPriceRange().lower.toFixed(4)} - {position.getPriceRange().upper.toFixed(4)}</p>
<p>Status: {position.isActive() ? '🟢 Active' : '🔴 Inactive'}</p>
<p>Value: ${position.getTotalValue().toFixed(2)}</p>
</div>
<div className="position-actions">
<button onClick={() => setSelectedPosition(position)}>Manage</button>
<button onClick={() => position.collectFees()}>Collect Fees</button>
<button onClick={() => closePosition(position)}>Close</button>
</div>
</div>
))}
</div>
</section>

{/* Position Details */}
{selectedPosition && (
<section className="position-details-panel">
<h3>Position Details</h3>
<PositionDetailsPanel
position={selectedPosition}
onUpdate={loadUserPositions}
/>
</section>
)}
</div>
);
}

Next Steps

✅ DLMM position management mastered
➡️ Next: Rust SDK Documentation

Or dive into practical applications: