What You’ll Achieve
- 20-50% better swap rates for users
- No disruption to existing functionality
- Easy rollback if needed
- Improved user satisfaction and retention
When to Use This Approach
- You have working swap functionality but want better rates
- Users are losing money to slippage on large trades
- You want to test Saros without breaking existing features
- You need competitive rates to retain users
Integration Scenarios
This guide covers common integration patterns:- Portfolio Tracker
- DeFi Dashboard
- DEX Aggregator
Adding: Swap functionality to existing token portfolio view
User flow: View token → Click swap → Execute via Saros DLMM
Integration Strategy
1. Non-Disruptive Installation
Add Saros SDK without affecting existing dependencies:Copy
# Install only what you need
npm install @saros-finance/dlmm-sdk
# No need to change existing Solana packages
# Works with your current @solana/web3.js version
2. Create Isolated Swap Hook
Copy
// hooks/useSarosSwap.ts
import { useState, useCallback } from 'react';
import { useConnection, useWallet } from '@solana/wallet-adapter-react';
import { PublicKey } from '@solana/web3.js';
import { LiquidityBookServices } from '@saros-finance/dlmm-sdk';
interface SwapResult {
signature: string;
amountIn: string;
amountOut: string;
priceImpact: number;
}
interface SwapError {
code: string;
message: string;
}
export const useSarosSwap = () => {
const { connection } = useConnection();
const { publicKey, signTransaction } = useWallet();
const [isSwapping, setIsSwapping] = useState(false);
const [lastSwap, setLastSwap] = useState<SwapResult | null>(null);
const [error, setError] = useState<SwapError | null>(null);
// Initialize DLMM service
const dlmmService = new LiquidityBookServices(connection);
const getSwapQuote = useCallback(async (
fromMint: string,
toMint: string,
amount: string,
decimalsIn: number,
decimalsOut: number,
slippage: number = 0.5
) => {
if (!publicKey) throw new Error('Wallet not connected');
try {
const pairAddress = await dlmmService.findBestPair(
new PublicKey(fromMint),
new PublicKey(toMint)
);
const quote = await dlmmService.getQuote({
pair: pairAddress,
tokenBase: new PublicKey(fromMint),
tokenQuote: new PublicKey(toMint),
amount: BigInt(parseFloat(amount) * Math.pow(10, decimalsIn)),
swapForY: true,
isExactInput: true,
tokenBaseDecimal: decimalsIn,
tokenQuoteDecimal: decimalsOut,
slippage
});
return {
amountOut: (Number(quote.amountOut) / Math.pow(10, decimalsOut)).toString(),
priceImpact: quote.priceImpact,
route: quote.route,
fee: quote.fee
};
} catch (err) {
console.error('Quote failed:', err);
throw err;
}
}, [publicKey, dlmmService]);
const executeSwap = useCallback(async (
fromMint: string,
toMint: string,
amount: string,
decimalsIn: number,
decimalsOut: number,
slippage: number = 0.5
) => {
if (!publicKey || !signTransaction) throw new Error('Wallet not connected');
setIsSwapping(true);
setError(null);
try {
// Get fresh quote
const quote = await getSwapQuote(fromMint, toMint, amount, decimalsIn, decimalsOut, slippage);
const pairAddress = await dlmmService.findBestPair(
new PublicKey(fromMint),
new PublicKey(toMint)
);
// Execute swap
const transaction = await dlmmService.swap({
pair: pairAddress,
tokenBase: new PublicKey(fromMint),
tokenQuote: new PublicKey(toMint),
amount: BigInt(parseFloat(amount) * Math.pow(10, decimalsIn)),
swapForY: true,
isExactInput: true,
wallet: publicKey,
slippage
});
const signedTx = await signTransaction(transaction);
const signature = await connection.sendRawTransaction(signedTx.serialize());
await connection.confirmTransaction(signature, 'confirmed');
const result: SwapResult = {
signature,
amountIn: amount,
amountOut: quote.amountOut,
priceImpact: quote.priceImpact
};
setLastSwap(result);
return result;
} catch (err: any) {
const swapError: SwapError = {
code: err.code || 'SWAP_FAILED',
message: err.message || 'Swap execution failed'
};
setError(swapError);
throw swapError;
} finally {
setIsSwapping(false);
}
}, [publicKey, signTransaction, connection, dlmmService, getSwapQuote]);
return {
getSwapQuote,
executeSwap,
isSwapping,
lastSwap,
error,
clearError: () => setError(null)
};
};
3. Create Compact Swap Component
Copy
// components/SarosSwapWidget.tsx
import React, { useState, useEffect } from 'react';
import { useSarosSwap } from '../hooks/useSarosSwap';
interface Token {
mint: string;
symbol: string;
decimals: number;
logoUri?: string;
}
interface SarosSwapWidgetProps {
tokens: Token[];
onSwapComplete?: (result: any) => void;
className?: string;
}
export const SarosSwapWidget: React.FC<SarosSwapWidgetProps> = ({
tokens,
onSwapComplete,
className = ''
}) => {
const { getSwapQuote, executeSwap, isSwapping, error } = useSarosSwap();
const [fromToken, setFromToken] = useState(tokens[0]);
const [toToken, setToToken] = useState(tokens[1]);
const [amount, setAmount] = useState('');
const [estimatedOutput, setEstimatedOutput] = useState('');
const [priceImpact, setPriceImpact] = useState(0);
// Auto-quote with debouncing
useEffect(() => {
if (!amount || !fromToken || !toToken || parseFloat(amount) <= 0) {
setEstimatedOutput('');
setPriceImpact(0);
return;
}
const timeoutId = setTimeout(async () => {
try {
const quote = await getSwapQuote(
fromToken.mint,
toToken.mint,
amount,
fromToken.decimals,
toToken.decimals
);
setEstimatedOutput(parseFloat(quote.amountOut).toFixed(6));
setPriceImpact(quote.priceImpact);
} catch (err) {
console.error('Quote failed:', err);
setEstimatedOutput('Error');
setPriceImpact(0);
}
}, 500);
return () => clearTimeout(timeoutId);
}, [amount, fromToken, toToken, getSwapQuote]);
const handleSwap = async () => {
try {
const result = await executeSwap(
fromToken.mint,
toToken.mint,
amount,
fromToken.decimals,
toToken.decimals
);
onSwapComplete?.(result);
setAmount('');
setEstimatedOutput('');
} catch (err) {
// Error handled by hook
}
};
const swapTokens = () => {
const temp = fromToken;
setFromToken(toToken);
setToToken(temp);
setAmount('');
setEstimatedOutput('');
};
return (
<div className={`saros-swap-widget ${className}`}>
<div className="swap-header">
<h3>⚡ Saros Swap</h3>
<span className="concentrated-liquidity-badge">Concentrated Liquidity</span>
</div>
{/* From Token */}
<div className="token-input">
<div className="token-input-header">
<span>From</span>
<select
value={fromToken.mint}
onChange={(e) => setFromToken(tokens.find(t => t.mint === e.target.value)!)}
>
{tokens.map(token => (
<option key={token.mint} value={token.mint}>
{token.symbol}
</option>
))}
</select>
</div>
<input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="0.00"
className="amount-input"
/>
</div>
{/* Swap Direction Button */}
<div className="swap-direction">
<button onClick={swapTokens} className="swap-direction-btn">
↕️
</button>
</div>
{/* To Token */}
<div className="token-input">
<div className="token-input-header">
<span>To</span>
<select
value={toToken.mint}
onChange={(e) => setToToken(tokens.find(t => t.mint === e.target.value)!)}
>
{tokens.map(token => (
<option key={token.mint} value={token.mint}>
{token.symbol}
</option>
))}
</select>
</div>
<input
type="text"
value={estimatedOutput}
placeholder="0.00"
disabled
className="amount-input"
/>
</div>
{/* Price Impact */}
{priceImpact > 0 && (
<div className="price-impact">
<span>Price Impact: </span>
<span className={priceImpact > 3 ? 'high-impact' : 'low-impact'}>
{priceImpact.toFixed(2)}%
</span>
</div>
)}
{/* Error Display */}
{error && (
<div className="error-message">
{error.message}
</div>
)}
{/* Swap Button */}
<button
onClick={handleSwap}
disabled={!amount || !estimatedOutput || isSwapping || estimatedOutput === 'Error'}
className="swap-button"
>
{isSwapping ? '🔄 Swapping...' : '⚡ Execute Saros Swap'}
</button>
{/* Benefits Display */}
<div className="benefits">
<div className="benefit-item">
<span>🎯 Concentrated Liquidity</span>
</div>
<div className="benefit-item">
<span>💰 Better Prices</span>
</div>
<div className="benefit-item">
<span>⚡ Sub-1% Price Impact</span>
</div>
</div>
<style jsx>{`
.saros-swap-widget {
background: #f8f9fa;
border-radius: 12px;
padding: 1rem;
border: 1px solid #e1e8ed;
max-width: 400px;
}
.swap-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.concentrated-liquidity-badge {
background: #4CAF50;
color: white;
padding: 0.25rem 0.5rem;
border-radius: 12px;
font-size: 0.8rem;
font-weight: bold;
}
.token-input {
background: white;
border-radius: 8px;
padding: 0.75rem;
margin-bottom: 0.5rem;
}
.token-input-header {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
font-size: 0.9rem;
color: #666;
}
.amount-input {
width: 100%;
border: none;
outline: none;
font-size: 1.1rem;
background: transparent;
}
.swap-direction {
text-align: center;
margin: 0.5rem 0;
}
.swap-direction-btn {
background: #6366F1;
color: white;
border: none;
border-radius: 50%;
width: 32px;
height: 32px;
cursor: pointer;
font-size: 1rem;
}
.price-impact {
font-size: 0.9rem;
margin: 0.5rem 0;
}
.low-impact {
color: #4CAF50;
}
.high-impact {
color: #f44336;
}
.error-message {
background: #ffebee;
color: #c62828;
padding: 0.5rem;
border-radius: 6px;
font-size: 0.9rem;
margin: 0.5rem 0;
}
.swap-button {
width: 100%;
background: #6366F1;
color: white;
border: none;
border-radius: 8px;
padding: 0.75rem;
font-size: 1rem;
font-weight: bold;
cursor: pointer;
margin-bottom: 1rem;
}
.swap-button:disabled {
background: #ccc;
cursor: not-allowed;
}
.benefits {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
font-size: 0.8rem;
}
.benefit-item {
text-align: center;
color: #666;
}
`}</style>
</div>
);
};
4. Integration into Existing App
Copy
// pages/portfolio.tsx (or your existing component)
import React from 'react';
import { SarosSwapWidget } from '../components/SarosSwapWidget';
// Your existing component
const PortfolioPage = () => {
// Your existing logic...
const supportedTokens = [
{
mint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
symbol: 'USDC',
decimals: 6
},
{
mint: 'So11111111111111111111111111111111111111112',
symbol: 'SOL',
decimals: 9
},
{
mint: 'C98A4nkJXhpVZNAZdHUA95RpTF3T4whtQubL3YobiUX9',
symbol: 'C98',
decimals: 6
}
];
const handleSwapComplete = (result: any) => {
console.log('Swap completed:', result);
// Optional: Refresh balances, show notification, etc.
// This integrates with your existing state management
refreshPortfolioBalances();
showSuccessNotification(`Swapped ${result.amountIn} for ${result.amountOut}`);
};
return (
<div className="portfolio-container">
{/* Your existing portfolio components */}
<div className="existing-portfolio-content">
{/* ... your existing JSX ... */}
</div>
{/* Add Saros swap widget */}
<div className="swap-widget-container">
<SarosSwapWidget
tokens={supportedTokens}
onSwapComplete={handleSwapComplete}
className="portfolio-swap-widget"
/>
</div>
</div>
);
};
5. Advanced Integration Patterns
A. Context-Based IntegrationCopy
// contexts/SarosContext.tsx
import React, { createContext, useContext, ReactNode } from 'react';
import { useSarosSwap } from '../hooks/useSarosSwap';
const SarosContext = createContext<ReturnType<typeof useSarosSwap> | null>(null);
export const SarosProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const sarosSwap = useSarosSwap();
return (
<SarosContext.Provider value={sarosSwap}>
{children}
</SarosContext.Provider>
);
};
export const useSaros = () => {
const context = useContext(SarosContext);
if (!context) {
throw new Error('useSaros must be used within SarosProvider');
}
return context;
};
Copy
// Track swap events in your existing analytics
const handleSwapComplete = (result: any) => {
// Your existing analytics (Google Analytics, Mixpanel, etc.)
analytics.track('Saros Swap Completed', {
fromToken: result.fromSymbol,
toToken: result.toSymbol,
amountIn: result.amountIn,
amountOut: result.amountOut,
priceImpact: result.priceImpact,
timestamp: Date.now()
});
};
Copy
// Integrate with your existing notification system
import { useToast } from '../hooks/useToast'; // Your existing hook
const { executeSwap, error } = useSarosSwap();
const { showToast } = useToast();
useEffect(() => {
if (error) {
showToast({
type: 'error',
message: `Swap failed: ${error.message}`,
duration: 5000
});
}
}, [error, showToast]);
Testing Integration
1. Development Testing
Copy
# Test in development environment
npm run dev
# Verify integration:
# 1. Existing functionality still works
# 2. Swap widget loads without errors
# 3. Wallet connection works for both existing and new features
# 4. No CSS conflicts or layout issues
2. Production Checklist
Copy
// production-config.ts
export const PRODUCTION_CONFIG = {
// Use mainnet endpoints for production
rpcEndpoint: process.env.NODE_ENV === 'production'
? 'https://api.mainnet-beta.solana.com'
: 'https://api.devnet.solana.com',
// Supported token list for production
supportedTokens: [
{
mint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC
symbol: 'USDC',
decimals: 6,
verified: true
},
{
mint: 'So11111111111111111111111111111111111111112', // SOL
symbol: 'SOL',
decimals: 9,
verified: true
}
// Add only verified tokens for production
],
// Conservative slippage for production
defaultSlippage: 0.5,
maxSlippage: 5.0,
// Error tracking
enableErrorReporting: true
};
3. Error Handling Strategy
Copy
// Graceful degradation
const SarosSwapWidget = ({ tokens, onSwapComplete }) => {
const [isSupported, setIsSupported] = useState(true);
useEffect(() => {
// Check if user's environment supports Saros
checkSarosSupport()
.then(setIsSupported)
.catch(() => setIsSupported(false));
}, []);
if (!isSupported) {
return (
<div className="swap-widget-unavailable">
<p>⚠️ Advanced swap features temporarily unavailable</p>
<p>Your existing functionality continues to work normally</p>
</div>
);
}
return <SarosSwapComponent {...props} />;
};
Production Error Handling & Recovery
Real applications need robust error handling for network failures, wallet issues, and transaction problems:Network & RPC Errors
Network & RPC Errors
Copy
// Enhanced error handling in useSarosSwap hook
const executeSwapWithRetry = useCallback(async (
swapParams: SwapParams,
maxRetries: number = 3
) => {
let lastError: Error;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await executeSwap(swapParams);
} catch (error: any) {
lastError = error;
// Handle specific error types
if (error.code === 'NETWORK_ERROR' || error.code === 'RPC_TIMEOUT') {
if (attempt < maxRetries) {
// Exponential backoff
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, attempt)));
continue;
}
}
// Don't retry for user errors
if (error.code === 'USER_REJECTED' || error.code === 'INSUFFICIENT_FUNDS') {
throw error;
}
// Don't retry for final attempt
if (attempt === maxRetries) break;
}
}
throw lastError!;
}, [executeSwap]);
Transaction Failure Recovery
Transaction Failure Recovery
Copy
// Transaction confirmation with timeout and fallback
const confirmTransactionWithFallback = async (signature: string) => {
const timeout = 30000; // 30 seconds
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
try {
const status = await connection.getSignatureStatus(signature);
if (status.value?.confirmationStatus === 'confirmed') {
return { success: true, signature };
}
if (status.value?.err) {
return {
success: false,
error: `Transaction failed: ${status.value.err}`,
signature
};
}
await new Promise(resolve => setTimeout(resolve, 2000));
} catch (error) {
console.warn('Status check failed, retrying...');
await new Promise(resolve => setTimeout(resolve, 3000));
}
}
// Timeout - transaction might still succeed
return {
success: false,
error: 'Transaction confirmation timeout - may still complete',
signature
};
};
User-Friendly Error Messages
User-Friendly Error Messages
Copy
const getUserFriendlyError = (error: any): string => {
const errorMap = {
'USER_REJECTED': 'Transaction was cancelled. Please try again.',
'INSUFFICIENT_FUNDS': 'Insufficient balance for this swap.',
'SLIPPAGE_EXCEEDED': 'Price moved too much. Increase slippage tolerance.',
'NETWORK_ERROR': 'Network connection issue. Please check your internet.',
'RPC_TIMEOUT': 'Request timed out. Please try again.',
'PAIR_NOT_FOUND': 'No liquidity available for this token pair.',
'AMOUNT_TOO_SMALL': 'Swap amount is too small. Minimum 0.001 tokens.',
'AMOUNT_TOO_LARGE': 'Swap amount exceeds available liquidity.'
};
return errorMap[error.code] || 'Swap failed. Please try again or contact support.';
};
// Usage in component
const handleSwap = async () => {
try {
setError(null);
const result = await executeSwapWithRetry(swapParams);
onSwapSuccess(result);
} catch (error: any) {
setError(getUserFriendlyError(error));
// Log for debugging but don't expose technical details
console.error('Swap failed:', {
code: error.code,
message: error.message,
stack: error.stack
});
}
};
Fallback UI States
Fallback UI States
Copy
// Component with comprehensive error states
return (
<div className="saros-swap-widget">
{error && (
<div className="error-banner">
<span className="error-icon">⚠️</span>
<div>
<p className="error-message">{error}</p>
{error.includes('network') && (
<button onClick={retryConnection} className="retry-btn">
Retry Connection
</button>
)}
{error.includes('slippage') && (
<button onClick={increaseSlippage} className="fix-btn">
Increase Slippage
</button>
)}
</div>
</div>
)}
{isSwapping ? (
<div className="swap-loading">
<div className="spinner" />
<p>Processing swap...</p>
<button onClick={cancelSwap} className="cancel-btn">
Cancel
</button>
</div>
) : (
<div className="swap-form">
{/* Normal swap interface */}
</div>
)}
</div>
);