Skip to main content
You need to build a professional trading interface but don’t want to spend months on complex AMM integrations. This guide shows you how to create swap and liquidity components that work with concentrated liquidity.

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

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

const { data: poolData } = useQuery(
  ['pool', poolAddress.toString()],
  () => getPoolAnalytics(poolAddress),
  { refetchInterval: 30000 }
);

With Zustand Store

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

export async function getServerSideProps() {
  const poolData = await getPoolData(poolAddress);
  return { props: { poolData } };
}

Best Practices

  1. Connection Management: Reuse connections across components
  2. Error Handling: Implement comprehensive error boundaries
  3. State Optimization: Use memo and callback hooks appropriately
  4. Performance: Implement proper loading and caching strategies
  5. User Experience: Provide real-time feedback and status updates