What You’ll Build
- Professional swap interface with real-time quotes
- Liquidity position management dashboard
- Error handling and user feedback
- Responsive design that works on all devices
React Application Architecture
DLMM React Integration Pattern
Component Data Flow
React Hook Example
Copy
import React, { createContext, useContext, useEffect, useState, useCallback } from 'react';
import { Connection, PublicKey } from '@solana/web3.js';
import { useWallet, useConnection } from '@solana/wallet-adapter-react';
import { LiquidityBookServices, MODE } from '@saros-finance/dlmm-sdk';
import { BN } from '@coral-xyz/anchor';
// DLMM Context Types
interface DLMMPool {
address: PublicKey;
tokenX: PublicKey;
tokenY: PublicKey;
binStep: number;
metadata: any; // Pool metadata from fetchPoolMetadata
}
interface DLMMContextState {
pools: DLMMPool[];
isLoading: boolean;
error: string | null;
service: LiquidityBookServices | null;
}
interface DLMMContextActions {
addPool: (poolAddress: PublicKey) => Promise<void>;
removePool: (poolAddress: PublicKey) => void;
getPoolMetadata: (poolAddress: string) => Promise<any>;
refreshPools: () => Promise<void>;
}
type DLMMContextType = DLMMContextState & DLMMContextActions;
// DLMM Context
const DLMMContext = createContext<DLMMContextType | undefined>(undefined);
/**
* DLMM Provider Component
*/
export const DLMMProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { connection } = useConnection();
const { publicKey } = useWallet();
const [state, setState] = useState<DLMMContextState>({
pools: [],
isLoading: false,
error: null,
service: null
});
// Initialize DLMM service
useEffect(() => {
if (connection) {
const service = new LiquidityBookServices({
mode: MODE.MAINNET, // or MODE.DEVNET based on connection
options: {
rpcUrl: connection.rpcEndpoint
}
});
setState(prev => ({ ...prev, service }));
}
}, [connection]);
const addPool = useCallback(async (poolAddress: PublicKey) => {
if (!connection) {
throw new Error('Connection not available');
}
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
// Check if pool already exists
const existing = state.activePools.get(poolAddress.toString());
if (existing) {
setState(prev => ({ ...prev, isLoading: false }));
return;
}
// Create DLMM instance
const dlmmPool = new DLMM(connection, poolAddress);
// Get pool information
const poolInfo = await dlmmPool.getPoolInfo();
const poolData: DLMMPool = {
address: poolAddress,
tokenX: poolInfo.tokenX,
tokenY: poolInfo.tokenY,
binStep: poolInfo.binStep,
instance: dlmmPool
};
setState(prev => ({
...prev,
pools: [...prev.pools, poolData],
activePools: new Map(prev.activePools).set(poolAddress.toString(), dlmmPool),
isLoading: false
}));
console.log('Pool added successfully:', poolAddress.toString());
} catch (error) {
console.error('Failed to add pool:', error);
setState(prev => ({
...prev,
error: error instanceof Error ? error.message : 'Failed to add pool',
isLoading: false
}));
}
}, [connection, state.activePools]);
const removePool = useCallback((poolAddress: PublicKey) => {
const poolKey = poolAddress.toString();
setState(prev => {
const newActivePools = new Map(prev.activePools);
newActivePools.delete(poolKey);
return {
...prev,
pools: prev.pools.filter(pool => pool.address.toString() !== poolKey),
activePools: newActivePools
};
});
console.log('Pool removed:', poolAddress.toString());
}, []);
const getPool = useCallback((poolAddress: PublicKey): DLMM | null => {
return state.activePools.get(poolAddress.toString()) || null;
}, [state.activePools]);
const refreshPools = useCallback(async () => {
setState(prev => ({ ...prev, isLoading: true }));
try {
// Refresh all pool data
for (const pool of state.pools) {
const poolInfo = await pool.instance.getPoolInfo();
// Update pool data as needed
}
setState(prev => ({ ...prev, isLoading: false }));
console.log('Pools refreshed successfully');
} catch (error) {
console.error('Failed to refresh pools:', error);
setState(prev => ({
...prev,
error: error instanceof Error ? error.message : 'Failed to refresh pools',
isLoading: false
}));
}
}, [state.pools]);
const contextValue: DLMMContextType = {
...state,
addPool,
removePool,
getPool,
refreshPools
};
return (
<DLMMContext.Provider value={contextValue}>
{children}
</DLMMContext.Provider>
);
};
/**
* Hook to use DLMM Context
*/
export const useDLMM = (): DLMMContextType => {
const context = useContext(DLMMContext);
if (!context) {
throw new Error('useDLMM must be used within a DLMMProvider');
}
return context;
};
/**
* Hook for DLMM Swap Operations
*/
export const useSwap = (poolAddress?: PublicKey) => {
const { publicKey, sendTransaction } = useWallet();
const { getPool } = useDLMM();
const [swapState, setSwapState] = useState({
isLoading: false,
error: null as string | null,
lastQuote: null as any,
lastTransaction: null as string | null
});
const getQuote = useCallback(async ({
amountIn,
swapYtoX = false
}: {
amountIn: number;
swapYtoX?: boolean;
}) => {
if (!poolAddress) {
throw new Error('Pool address not provided');
}
const pool = getPool(poolAddress);
if (!pool) {
throw new Error('Pool not found');
}
setSwapState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const quote = await pool.getQuote({
amountIn: new BN(amountIn * Math.pow(10, swapYtoX ? 9 : 6)), // Adjust decimals
swapYtoX
});
const processedQuote = {
amountIn,
amountOut: quote.amountOut.toNumber() / Math.pow(10, swapYtoX ? 6 : 9),
priceImpact: quote.priceImpact || 0,
fee: quote.fee.toNumber() / Math.pow(10, swapYtoX ? 9 : 6),
binsCrossed: quote.binsCrossed || 0,
minAmountOut: quote.minAmountOut.toNumber() / Math.pow(10, swapYtoX ? 6 : 9)
};
setSwapState(prev => ({
...prev,
lastQuote: processedQuote,
isLoading: false
}));
return processedQuote;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to get quote';
setSwapState(prev => ({
...prev,
error: errorMessage,
isLoading: false
}));
throw error;
}
}, [poolAddress, getPool]);
const executeSwap = useCallback(async ({
amountIn,
minAmountOut,
swapYtoX = false
}: {
amountIn: number;
minAmountOut: number;
swapYtoX?: boolean;
}) => {
if (!publicKey || !sendTransaction || !poolAddress) {
throw new Error('Wallet not connected or pool not available');
}
const pool = getPool(poolAddress);
if (!pool) {
throw new Error('Pool not found');
}
setSwapState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const swapInstruction = await pool.swap({
user: publicKey,
amountIn: new BN(amountIn * Math.pow(10, swapYtoX ? 9 : 6)),
minAmountOut: new BN(minAmountOut * Math.pow(10, swapYtoX ? 6 : 9)),
swapYtoX
});
const transaction = new Transaction().add(swapInstruction);
const signature = await sendTransaction(transaction, connection);
setSwapState(prev => ({
...prev,
lastTransaction: signature,
isLoading: false
}));
return signature;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Swap failed';
setSwapState(prev => ({
...prev,
error: errorMessage,
isLoading: false
}));
throw error;
}
}, [publicKey, sendTransaction, poolAddress, getPool]);
return {
...swapState,
getQuote,
executeSwap
};
};
/**
* Hook for DLMM Position Management
*/
export const usePositions = (poolAddress?: PublicKey) => {
const { publicKey, sendTransaction } = useWallet();
const { connection } = useConnection();
const { getPool } = useDLMM();
const [positions, setPositions] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchPositions = useCallback(async () => {
if (!publicKey || !poolAddress) return;
const pool = getPool(poolAddress);
if (!pool) return;
setIsLoading(true);
setError(null);
try {
const userPositions = await pool.getPositionsByUser(publicKey);
setPositions(userPositions);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch positions');
} finally {
setIsLoading(false);
}
}, [publicKey, poolAddress, getPool]);
const createPosition = useCallback(async ({
lowerBinId,
upperBinId,
amountX,
amountY,
slippage = 0.005
}: {
lowerBinId: number;
upperBinId: number;
amountX: number;
amountY: number;
slippage?: number;
}) => {
if (!publicKey || !sendTransaction || !poolAddress) {
throw new Error('Wallet not connected or pool not available');
}
const pool = getPool(poolAddress);
if (!pool) {
throw new Error('Pool not found');
}
setIsLoading(true);
setError(null);
try {
const createTx = await pool.initializePositionAndAddLiquidity({
user: publicKey,
lowerBinId,
upperBinId,
tokenXAmount: new BN(amountX * Math.pow(10, 6)),
tokenYAmount: new BN(amountY * Math.pow(10, 9)),
binLiquidityDist: [] // Calculate distribution
});
const signature = await sendTransaction(createTx, connection);
await fetchPositions(); // Refresh positions
return signature;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to create position';
setError(errorMessage);
throw err;
} finally {
setIsLoading(false);
}
}, [publicKey, sendTransaction, poolAddress, getPool, connection, fetchPositions]);
const addLiquidity = useCallback(async ({
positionAddress,
amountX,
amountY
}: {
positionAddress: PublicKey;
amountX: number;
amountY: number;
}) => {
if (!publicKey || !sendTransaction || !poolAddress) {
throw new Error('Wallet not connected or pool not available');
}
const pool = getPool(poolAddress);
if (!pool) {
throw new Error('Pool not found');
}
setIsLoading(true);
setError(null);
try {
const addTx = await pool.addLiquidityByStrategy({
user: publicKey,
position: positionAddress,
tokenXAmount: new BN(amountX * Math.pow(10, 6)),
tokenYAmount: new BN(amountY * Math.pow(10, 9)),
binLiquidityDist: [] // Calculate distribution
});
const signature = await sendTransaction(addTx, connection);
await fetchPositions(); // Refresh positions
return signature;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to add liquidity';
setError(errorMessage);
throw err;
} finally {
setIsLoading(false);
}
}, [publicKey, sendTransaction, poolAddress, getPool, connection, fetchPositions]);
const removeLiquidity = useCallback(async ({
positionAddress,
binIds,
liquidityAmounts
}: {
positionAddress: PublicKey;
binIds: number[];
liquidityAmounts: BN[];
}) => {
if (!publicKey || !sendTransaction || !poolAddress) {
throw new Error('Wallet not connected or pool not available');
}
const pool = getPool(poolAddress);
if (!pool) {
throw new Error('Pool not found');
}
setIsLoading(true);
setError(null);
try {
const removeTx = await pool.removeLiquidity({
user: publicKey,
position: positionAddress,
binIds,
liquidityAmounts
});
const signature = await sendTransaction(removeTx, connection);
await fetchPositions(); // Refresh positions
return signature;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to remove liquidity';
setError(errorMessage);
throw err;
} finally {
setIsLoading(false);
}
}, [publicKey, sendTransaction, poolAddress, getPool, connection, fetchPositions]);
// Fetch positions when dependencies change
useEffect(() => {
fetchPositions();
}, [fetchPositions]);
return {
positions,
isLoading,
error,
createPosition,
addLiquidity,
removeLiquidity,
refreshPositions: fetchPositions
};
};
/**
* Hook for Pool Analytics and Data
*/
export const usePoolData = (poolAddress?: PublicKey) => {
const { getPool } = useDLMM();
const [poolData, setPoolData] = useState<any>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchPoolData = useCallback(async () => {
if (!poolAddress) return;
const pool = getPool(poolAddress);
if (!pool) return;
setIsLoading(true);
setError(null);
try {
const [poolState, poolInfo] = await Promise.all([
pool.getPoolState(),
pool.getPoolInfo()
]);
// Get liquidity distribution around active bin
const activeBins = [];
for (let i = -20; i <= 20; i++) {
try {
const bin = await pool.getBin(poolState.activeId + i);
activeBins.push({
binId: poolState.activeId + i,
liquiditySupply: bin.liquiditySupply.toNumber(),
amountX: bin.amountX.toNumber(),
amountY: bin.amountY.toNumber()
});
} catch {
// Bin doesn't exist
}
}
const data = {
address: poolAddress.toString(),
tokenX: poolInfo.tokenX.toString(),
tokenY: poolInfo.tokenY.toString(),
binStep: poolInfo.binStep,
state: {
activeId: poolState.activeId,
reserveX: poolState.reserveX.toNumber(),
reserveY: poolState.reserveY.toNumber(),
liquiditySupply: poolState.liquiditySupply.toNumber()
},
bins: activeBins,
currentPrice: Math.pow(1 + poolInfo.binStep / 10000, poolState.activeId - 8388608)
};
setPoolData(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch pool data');
} finally {
setIsLoading(false);
}
}, [poolAddress, getPool]);
// Auto-refresh pool data
useEffect(() => {
fetchPoolData();
const interval = setInterval(fetchPoolData, 30000); // Refresh every 30s
return () => clearInterval(interval);
}, [fetchPoolData]);
return {
poolData,
isLoading,
error,
refreshPoolData: fetchPoolData
};
};
/**
* Example React Component using DLMM hooks
*/
export const DLMMTradingComponent: React.FC<{ poolAddress: PublicKey }> = ({ poolAddress }) => {
const [amountIn, setAmountIn] = useState('');
const [amountOut, setAmountOut] = useState('');
const [slippage, setSlippage] = useState(0.5);
const { isLoading: swapLoading, getQuote, executeSwap, lastQuote, error: swapError } = useSwap(poolAddress);
const { poolData, isLoading: poolLoading } = usePoolData(poolAddress);
const { positions, createPosition, isLoading: positionLoading } = usePositions(poolAddress);
// Auto-update quote when amount changes
useEffect(() => {
if (amountIn && parseFloat(amountIn) > 0) {
getQuote({ amountIn: parseFloat(amountIn) });
}
}, [amountIn, getQuote]);
// Update output amount from quote
useEffect(() => {
if (lastQuote) {
setAmountOut(lastQuote.amountOut.toFixed(6));
}
}, [lastQuote]);
const handleSwap = async () => {
if (!lastQuote || !amountIn) return;
try {
const minAmountOut = lastQuote.amountOut * (1 - slippage / 100);
const signature = await executeSwap({
amountIn: parseFloat(amountIn),
minAmountOut
});
alert(`Swap successful! Transaction: ${signature}`);
setAmountIn('');
setAmountOut('');
} catch (error) {
alert(`Swap failed: ${error}`);
}
};
const handleCreatePosition = async () => {
try {
const signature = await createPosition({
lowerBinId: poolData?.state.activeId - 10,
upperBinId: poolData?.state.activeId + 10,
amountX: 100, // 100 USDC
amountY: 1 // 1 SOL
});
alert(`Position created! Transaction: ${signature}`);
} catch (error) {
alert(`Position creation failed: ${error}`);
}
};
if (poolLoading) {
return <div className="p-4">Loading pool data...</div>;
}
return (
<div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow">
<h2 className="text-2xl font-bold mb-6">DLMM Trading</h2>
{/* Pool Information */}
{poolData && (
<div className="mb-6 p-4 bg-gray-50 rounded">
<h3 className="font-semibold">Pool Info</h3>
<p>Current Price: {poolData.currentPrice.toFixed(6)}</p>
<p>Active Bin: {poolData.state.activeId}</p>
<p>TVL: ${((poolData.state.reserveX / 1e6) + (poolData.state.reserveY / 1e9 * 105)).toLocaleString()}</p>
</div>
)}
{/* Swap Interface */}
<div className="mb-6">
<h3 className="font-semibold mb-3">Swap</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">Amount In (USDC)</label>
<input
type="number"
value={amountIn}
onChange={(e) => setAmountIn(e.target.value)}
className="w-full p-2 border rounded"
placeholder="0.0"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Amount Out (SOL)</label>
<input
type="text"
value={amountOut}
readOnly
className="w-full p-2 border rounded bg-gray-50"
placeholder="0.0"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Slippage (%)</label>
<input
type="number"
value={slippage}
onChange={(e) => setSlippage(parseFloat(e.target.value))}
className="w-full p-2 border rounded"
step="0.1"
/>
</div>
{lastQuote && (
<div className="p-3 bg-blue-50 rounded text-sm">
<p>Price Impact: {lastQuote.priceImpact.toFixed(2)}%</p>
<p>Fee: {lastQuote.fee.toFixed(4)} USDC</p>
<p>Bins Crossed: {lastQuote.binsCrossed}</p>
</div>
)}
{swapError && (
<div className="p-3 bg-red-50 text-red-700 rounded text-sm">
{swapError}
</div>
)}
<button
onClick={handleSwap}
disabled={swapLoading || !lastQuote || !amountIn}
className="w-full py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
>
{swapLoading ? 'Swapping...' : 'Swap'}
</button>
</div>
</div>
{/* Position Management */}
<div>
<h3 className="font-semibold mb-3">Liquidity Positions</h3>
<div className="space-y-3">
<div className="text-sm text-gray-600">
Active Positions: {positions.length}
</div>
<button
onClick={handleCreatePosition}
disabled={positionLoading}
className="w-full py-2 bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50"
>
{positionLoading ? 'Creating...' : 'Create Position (100 USDC + 1 SOL)'}
</button>
{positions.map((position, index) => (
<div key={index} className="p-3 bg-gray-50 rounded text-sm">
<p>Position {index + 1}</p>
<p>Range: {position.lowerBinId} - {position.upperBinId}</p>
<p>Liquidity: {position.liquidityShare?.toString()}</p>
</div>
))}
</div>
</div>
</div>
);
};
/**
* Main App Component with DLMM Integration
*/
export const DLMMApp: React.FC = () => {
const poolAddress = new PublicKey('YourPoolAddress');
return (
<DLMMProvider>
<div className="min-h-screen bg-gray-100 py-8">
<div className="container mx-auto px-4">
<h1 className="text-4xl font-bold text-center mb-8">DLMM Trading App</h1>
<DLMMTradingComponent poolAddress={poolAddress} />
</div>
</div>
</DLMMProvider>
);
};
export default DLMMApp;
React Integration Features
Context Provider Pattern
- DLMMProvider: Centralized pool management and state
- Connection Management: Automatic RPC connection handling
- Pool Registry: Maintain active pool instances
- Error Handling: Comprehensive error states and recovery
Custom Hooks
- useDLMM: Core DLMM context and pool management
- useSwap: Swap operations with quote management
- usePositions: Liquidity position CRUD operations
- usePoolData: Real-time pool state and analytics
Component Architecture
- Modular Design: Reusable hooks and components
- State Management: Local and global state coordination
- Real-time Updates: Automatic data refreshing
- Error Boundaries: Graceful error handling
Advanced Features
- Auto-refresh: Periodic data updates
- Optimistic Updates: Immediate UI feedback
- Transaction Tracking: Complete transaction lifecycle
- Performance Optimization: Efficient re-rendering
Integration Patterns
With React Query
Copy
const { data: poolData } = useQuery(
['pool', poolAddress.toString()],
() => getPoolAnalytics(poolAddress),
{ refetchInterval: 30000 }
);
With Zustand Store
Copy
const useDLMMStore = create((set) => ({
pools: [],
addPool: (pool) => set((state) => ({ pools: [...state.pools, pool] })),
removePool: (address) => set((state) => ({
pools: state.pools.filter(p => p.address !== address)
}))
}));
With Next.js SSR
Copy
export async function getServerSideProps() {
const poolData = await getPoolData(poolAddress);
return { props: { poolData } };
}
Best Practices
- Connection Management: Reuse connections across components
- Error Handling: Implement comprehensive error boundaries
- State Optimization: Use memo and callback hooks appropriately
- Performance: Implement proper loading and caching strategies
- User Experience: Provide real-time feedback and status updates