Skip to main content

Example: DeFi Analytics Dashboard

A comprehensive portfolio analytics dashboard that tracks all your Saros positions, calculates yields, monitors performance, and provides actionable insights.

Overview

This example demonstrates:

  • Real-time portfolio tracking across all Saros products
  • Yield farming analytics and APY calculations
  • Impermanent loss tracking for LP positions
  • Historical performance analysis
  • Risk metrics and position monitoring
  • Automated rebalancing suggestions

Complete Implementation

Project Setup

# Create project
mkdir saros-analytics-dashboard
cd saros-analytics-dashboard

# Initialize with TypeScript support
npm init -y
npm install @saros-finance/sdk @saros-finance/dlmm-sdk @solana/web3.js
npm install express cors dotenv node-cron
npm install -D @types/node @types/express typescript ts-node nodemon

# Frontend dependencies
npm install next react react-dom chart.js react-chartjs-2
npm install -D @types/react @types/react-dom tailwindcss

Core Analytics Engine

// src/analytics/PortfolioAnalyzer.ts
import { Connection, PublicKey } from '@solana/web3.js';
import {
getStakeInfo,
getFarmInfo,
getPoolInfo,
getAllPools,
getTokenMetadata,
convertWeiToBalance
} from '@saros-finance/sdk';
import { PositionManager } from '@saros-finance/dlmm-sdk';
import BN from 'bn.js';

export class PortfolioAnalyzer {
private connection: Connection;
private walletAddress: PublicKey;
private priceCache: Map<string, number> = new Map();
private historicalData: Map<string, PriceHistory[]> = new Map();

constructor(connection: Connection, walletAddress: string) {
this.connection = connection;
this.walletAddress = new PublicKey(walletAddress);
}

async analyzeCompletePortfolio(): Promise<PortfolioAnalysis> {
console.log('🔍 Analyzing complete portfolio for:', this.walletAddress.toString());

try {
const [
stakingPositions,
farmingPositions,
liquidityPositions,
dlmmPositions,
tokenBalances
] = await Promise.all([
this.getStakingPositions(),
this.getFarmingPositions(),
this.getLiquidityPositions(),
this.getDLMMPositions(),
this.getTokenBalances()
]);

// Calculate total values
const totalStaked = this.calculateTotalValue(stakingPositions);
const totalFarming = this.calculateTotalValue(farmingPositions);
const totalLiquidity = this.calculateTotalValue(liquidityPositions);
const totalDLMM = this.calculateTotalValue(dlmmPositions);
const totalTokens = this.calculateTotalValue(tokenBalances);

const totalPortfolioValue = totalStaked + totalFarming + totalLiquidity + totalDLMM + totalTokens;

// Calculate yields and performance
const stakingYield = await this.calculateStakingYield(stakingPositions);
const farmingYield = await this.calculateFarmingYield(farmingPositions);
const liquidityYield = await this.calculateLiquidityYield(liquidityPositions);
const dlmmYield = await this.calculateDLMMYield(dlmmPositions);

const totalDailyYield = stakingYield.daily + farmingYield.daily + liquidityYield.daily + dlmmYield.daily;
const totalAPY = (totalDailyYield * 365 / totalPortfolioValue) * 100;

// Risk analysis
const riskMetrics = await this.calculateRiskMetrics({
stakingPositions,
farmingPositions,
liquidityPositions,
dlmmPositions
});

// Rebalancing suggestions
const rebalancingSuggestions = await this.generateRebalancingSuggestions({
totalPortfolioValue,
stakingPositions,
farmingPositions,
liquidityPositions,
dlmmPositions
});

const analysis: PortfolioAnalysis = {
timestamp: new Date(),
walletAddress: this.walletAddress.toString(),
totalValue: totalPortfolioValue,
breakdown: {
staking: { value: totalStaked, percentage: (totalStaked / totalPortfolioValue) * 100 },
farming: { value: totalFarming, percentage: (totalFarming / totalPortfolioValue) * 100 },
liquidity: { value: totalLiquidity, percentage: (totalLiquidity / totalPortfolioValue) * 100 },
dlmm: { value: totalDLMM, percentage: (totalDLMM / totalPortfolioValue) * 100 },
tokens: { value: totalTokens, percentage: (totalTokens / totalPortfolioValue) * 100 }
},
yields: {
staking: stakingYield,
farming: farmingYield,
liquidity: liquidityYield,
dlmm: dlmmYield,
total: {
daily: totalDailyYield,
weekly: totalDailyYield * 7,
monthly: totalDailyYield * 30,
yearly: totalDailyYield * 365,
apy: totalAPY
}
},
positions: {
staking: stakingPositions,
farming: farmingPositions,
liquidity: liquidityPositions,
dlmm: dlmmPositions
},
riskMetrics,
rebalancingSuggestions,
performance: await this.calculateHistoricalPerformance()
};

console.log('✅ Portfolio analysis complete');
console.log(`💰 Total Value: $${totalPortfolioValue.toFixed(2)}`);
console.log(`📈 Total APY: ${totalAPY.toFixed(2)}%`);
console.log(`💵 Daily Yield: $${totalDailyYield.toFixed(2)}`);

return analysis;
} catch (error) {
console.error('❌ Portfolio analysis failed:', error);
throw error;
}
}

private async getStakingPositions(): Promise<StakingPosition[]> {
console.log('📊 Fetching staking positions...');

const positions: StakingPosition[] = [];

try {
// Get all staking pools
const stakingPools = await this.getAllStakingPools();

for (const pool of stakingPools) {
const stakeInfo = await getStakeInfo(
this.connection,
this.walletAddress,
new PublicKey(pool.address)
);

if (stakeInfo && stakeInfo.stakedAmount > 0) {
const tokenPrice = await this.getTokenPrice(pool.stakingToken);
const stakedValue = stakeInfo.stakedAmount * tokenPrice;
const pendingRewardsValue = stakeInfo.pendingRewards * await this.getTokenPrice(pool.rewardToken);

positions.push({
poolAddress: pool.address,
stakingToken: pool.stakingToken,
rewardToken: pool.rewardToken,
stakedAmount: stakeInfo.stakedAmount,
stakedValue,
pendingRewards: stakeInfo.pendingRewards,
pendingRewardsValue,
apy: pool.apy,
lockEndTime: stakeInfo.lockEndTime,
canUnstake: stakeInfo.canUnstake,
lockPeriod: pool.lockPeriod,
multiplier: stakeInfo.multiplier || 1
});
}
}

console.log(`✅ Found ${positions.length} staking positions`);
return positions;
} catch (error) {
console.error('❌ Failed to fetch staking positions:', error);
return [];
}
}

private async getFarmingPositions(): Promise<FarmingPosition[]> {
console.log('🚜 Fetching farming positions...');

const positions: FarmingPosition[] = [];

try {
const farmingPools = await this.getAllFarmingPools();

for (const pool of farmingPools) {
const farmInfo = await getFarmInfo(
this.connection,
this.walletAddress,
new PublicKey(pool.address)
);

if (farmInfo && farmInfo.deposited > 0) {
// Get LP token price
const lpTokenPrice = await this.getLPTokenPrice(pool.lpTokenMint);
const depositedValue = farmInfo.deposited * lpTokenPrice;

// Calculate pending rewards value
let totalPendingRewardsValue = 0;
for (const reward of farmInfo.pendingRewards) {
const rewardPrice = await this.getTokenPrice(reward.token);
totalPendingRewardsValue += reward.amount * rewardPrice;
}

positions.push({
poolAddress: pool.address,
lpTokenMint: pool.lpTokenMint,
underlyingPool: pool.underlyingPool,
depositedAmount: farmInfo.deposited,
depositedValue,
pendingRewards: farmInfo.pendingRewards,
totalPendingRewardsValue,
apy: farmInfo.apy,
userShare: farmInfo.userShare,
dailyRewards: farmInfo.dailyRewards,
rewardTokens: farmInfo.rewardTokens
});
}
}

console.log(`✅ Found ${positions.length} farming positions`);
return positions;
} catch (error) {
console.error('❌ Failed to fetch farming positions:', error);
return [];
}
}

private async getLiquidityPositions(): Promise<LiquidityPosition[]> {
console.log('💧 Fetching liquidity positions...');

const positions: LiquidityPosition[] = [];

try {
const allPools = await getAllPools(this.connection);

for (const pool of allPools) {
const lpBalance = await this.getLPTokenBalance(pool.lpMint);

if (lpBalance > 0) {
const lpTokenPrice = await this.getLPTokenPrice(pool.lpMint);
const totalValue = lpBalance * lpTokenPrice;

// Calculate underlying token amounts
const { tokenAAmount, tokenBAmount } = await this.calculateLPTokenValue(
pool,
lpBalance
);

const tokenAPrice = await this.getTokenPrice(pool.tokenA.mint);
const tokenBPrice = await this.getTokenPrice(pool.tokenB.mint);

const tokenAValue = tokenAAmount * tokenAPrice;
const tokenBValue = tokenBAmount * tokenBPrice;

// Calculate impermanent loss
const impermanentLoss = await this.calculateImpermanentLoss(
pool,
tokenAAmount,
tokenBAmount,
tokenAPrice,
tokenBPrice
);

// Calculate fees earned
const feesEarned = await this.calculateFeesEarned(pool.address, lpBalance);

positions.push({
poolAddress: pool.address,
lpTokenMint: pool.lpMint,
lpBalance,
totalValue,
tokenA: {
mint: pool.tokenA.mint,
amount: tokenAAmount,
value: tokenAValue,
symbol: pool.tokenA.symbol
},
tokenB: {
mint: pool.tokenB.mint,
amount: tokenBAmount,
value: tokenBValue,
symbol: pool.tokenB.symbol
},
impermanentLoss,
feesEarned,
apy: await this.calculateLiquidityAPY(pool),
shareOfPool: (lpBalance / pool.lpSupply) * 100
});
}
}

console.log(`✅ Found ${positions.length} liquidity positions`);
return positions;
} catch (error) {
console.error('❌ Failed to fetch liquidity positions:', error);
return [];
}
}

private async getDLMMPositions(): Promise<DLMMPosition[]> {
console.log('🎯 Fetching DLMM positions...');

const positions: DLMMPosition[] = [];

try {
const positionManager = new PositionManager(this.connection);
const userPositions = await positionManager.getUserPositions(this.walletAddress);

for (const position of userPositions) {
const poolInfo = await positionManager.getPoolInfo(position.poolAddress);
const tokenAPrice = await this.getTokenPrice(poolInfo.tokenA.mint);
const tokenBPrice = await this.getTokenPrice(poolInfo.tokenB.mint);

const tokenAValue = position.tokenAAmount * tokenAPrice;
const tokenBValue = position.tokenBAmount * tokenBPrice;
const totalValue = tokenAValue + tokenBValue;

// Calculate fees earned
const feesEarned = position.feeTokenA * tokenAPrice + position.feeTokenB * tokenBPrice;

// Calculate position health
const currentPrice = tokenBPrice / tokenAPrice;
const isInRange = currentPrice >= position.lowerPrice && currentPrice <= position.upperPrice;

positions.push({
positionId: position.id,
poolAddress: position.poolAddress.toString(),
tokenA: {
mint: poolInfo.tokenA.mint,
amount: position.tokenAAmount,
value: tokenAValue,
symbol: poolInfo.tokenA.symbol
},
tokenB: {
mint: poolInfo.tokenB.mint,
amount: position.tokenBAmount,
value: tokenBValue,
symbol: poolInfo.tokenB.symbol
},
totalValue,
lowerPrice: position.lowerPrice,
upperPrice: position.upperPrice,
currentPrice,
isInRange,
liquidity: position.liquidity,
feesEarned,
apy: await this.calculateDLMMApy(position),
utilization: this.calculatePositionUtilization(position, currentPrice)
});
}

console.log(`✅ Found ${positions.length} DLMM positions`);
return positions;
} catch (error) {
console.error('❌ Failed to fetch DLMM positions:', error);
return [];
}
}

private async calculateStakingYield(positions: StakingPosition[]): Promise<YieldInfo> {
let totalDailyYield = 0;

for (const position of positions) {
const dailyYield = (position.stakedValue * position.apy / 100) / 365;
totalDailyYield += dailyYield;
}

return {
daily: totalDailyYield,
weekly: totalDailyYield * 7,
monthly: totalDailyYield * 30,
yearly: totalDailyYield * 365,
apy: positions.length > 0
? positions.reduce((sum, p) => sum + (p.apy * p.stakedValue), 0) /
positions.reduce((sum, p) => sum + p.stakedValue, 0)
: 0
};
}

private async calculateFarmingYield(positions: FarmingPosition[]): Promise<YieldInfo> {
let totalDailyYield = 0;

for (const position of positions) {
totalDailyYield += position.dailyRewards;
}

return {
daily: totalDailyYield,
weekly: totalDailyYield * 7,
monthly: totalDailyYield * 30,
yearly: totalDailyYield * 365,
apy: positions.length > 0
? positions.reduce((sum, p) => sum + (p.apy * p.depositedValue), 0) /
positions.reduce((sum, p) => sum + p.depositedValue, 0)
: 0
};
}

private async calculateRiskMetrics(positions: any): Promise<RiskMetrics> {
const totalValue = Object.values(positions).flat().reduce((sum: number, pos: any) =>
sum + (pos.totalValue || pos.stakedValue || pos.depositedValue || 0), 0);

// Calculate diversification score (0-100)
const positionTypes = Object.keys(positions).length;
const diversificationScore = Math.min(positionTypes * 20, 100);

// Calculate concentration risk
const largestPosition = Math.max(...Object.values(positions).flat().map((pos: any) =>
pos.totalValue || pos.stakedValue || pos.depositedValue || 0));
const concentrationRisk = (largestPosition / totalValue) * 100;

// Calculate impermanent loss risk
const liquidityPositions = positions.liquidityPositions as LiquidityPosition[];
const dlmmPositions = positions.dlmmPositions as DLMMPosition[];

let totalILRisk = 0;
let ilExposedValue = 0;

// Add liquidity position IL risk
for (const pos of liquidityPositions || []) {
totalILRisk += Math.abs(pos.impermanentLoss) * pos.totalValue;
ilExposedValue += pos.totalValue;
}

// Add DLMM position IL risk (reduced due to concentrated liquidity)
for (const pos of dlmmPositions || []) {
const ilRisk = pos.isInRange ? 0.1 : 0.3; // Lower risk when in range
totalILRisk += ilRisk * pos.totalValue;
ilExposedValue += pos.totalValue;
}

const avgImpermanentLossRisk = ilExposedValue > 0 ? totalILRisk / ilExposedValue : 0;

// Calculate smart contract risk (based on position types and amounts)
const smartContractRisk = Math.min((totalValue / 100000) * 10 + positionTypes * 5, 50);

return {
diversificationScore,
concentrationRisk,
impermanentLossRisk: avgImpermanentLossRisk,
smartContractRisk,
overallRisk: (100 - diversificationScore + concentrationRisk + avgImpermanentLossRisk + smartContractRisk) / 4
};
}

private async generateRebalancingSuggestions(data: any): Promise<RebalancingSuggestion[]> {
const suggestions: RebalancingSuggestion[] = [];
const { totalPortfolioValue, stakingPositions, farmingPositions, liquidityPositions, dlmmPositions } = data;

// Suggest diversification if concentration is too high
const stakingValue = stakingPositions.reduce((sum: number, p: any) => sum + p.stakedValue, 0);
const stakingPercentage = (stakingValue / totalPortfolioValue) * 100;

if (stakingPercentage > 70) {
suggestions.push({
type: 'diversify',
priority: 'high',
title: 'Reduce Staking Concentration',
description: `${stakingPercentage.toFixed(1)}% of portfolio is in staking. Consider moving some funds to liquidity or farming for better diversification.`,
impact: 'Reduce concentration risk',
estimatedGain: 'Lower risk profile'
});
}

// Suggest harvesting rewards
const totalPendingRewards = [
...stakingPositions.map((p: any) => p.pendingRewardsValue),
...farmingPositions.map((p: any) => p.totalPendingRewardsValue)
].reduce((sum, val) => sum + val, 0);

if (totalPendingRewards > 50) {
suggestions.push({
type: 'harvest',
priority: 'medium',
title: 'Harvest Pending Rewards',
description: `You have $${totalPendingRewards.toFixed(2)} in unharvested rewards.`,
impact: 'Compound earnings',
estimatedGain: `$${totalPendingRewards.toFixed(2)} immediate gain`
});
}

// Suggest rebalancing DLMM positions
const outOfRangeDLMM = dlmmPositions.filter((p: any) => !p.isInRange);
if (outOfRangeDLMM.length > 0) {
const outOfRangeValue = outOfRangeDLMM.reduce((sum: number, p: any) => sum + p.totalValue, 0);
suggestions.push({
type: 'rebalance',
priority: 'high',
title: 'Rebalance Out-of-Range DLMM Positions',
description: `${outOfRangeDLMM.length} DLMM positions ($${outOfRangeValue.toFixed(2)}) are out of range and not earning fees.`,
impact: 'Resume fee generation',
estimatedGain: 'Restore APY on idle liquidity'
});
}

return suggestions;
}

// Helper methods
private async getTokenPrice(mint: string): Promise<number> {
if (this.priceCache.has(mint)) {
return this.priceCache.get(mint)!;
}

try {
// In production, use real price feeds (Jupiter, Pyth, etc.)
const mockPrices: Record<string, number> = {
'So11111111111111111111111111111111111111112': 180.50, // SOL
'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v': 1.00, // USDC
'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB': 0.9998, // USDT
'C98A4nkJXhpVZNAZdHUA95RpTF3T4whtQubL3YobiUX9': 0.45, // C98
};

const price = mockPrices[mint] || 1.0;
this.priceCache.set(mint, price);
return price;
} catch (error) {
console.warn(`Failed to fetch price for ${mint}:`, error);
return 1.0;
}
}

private calculateTotalValue(positions: any[]): number {
return positions.reduce((total, pos) =>
total + (pos.totalValue || pos.stakedValue || pos.depositedValue || pos.value || 0), 0
);
}

// ... Additional helper methods would be implemented here
}

// Type definitions
export interface PortfolioAnalysis {
timestamp: Date;
walletAddress: string;
totalValue: number;
breakdown: {
staking: { value: number; percentage: number };
farming: { value: number; percentage: number };
liquidity: { value: number; percentage: number };
dlmm: { value: number; percentage: number };
tokens: { value: number; percentage: number };
};
yields: {
staking: YieldInfo;
farming: YieldInfo;
liquidity: YieldInfo;
dlmm: YieldInfo;
total: YieldInfo;
};
positions: {
staking: StakingPosition[];
farming: FarmingPosition[];
liquidity: LiquidityPosition[];
dlmm: DLMMPosition[];
};
riskMetrics: RiskMetrics;
rebalancingSuggestions: RebalancingSuggestion[];
performance: PerformanceMetrics;
}

export interface StakingPosition {
poolAddress: string;
stakingToken: string;
rewardToken: string;
stakedAmount: number;
stakedValue: number;
pendingRewards: number;
pendingRewardsValue: number;
apy: number;
lockEndTime: number;
canUnstake: boolean;
lockPeriod: number;
multiplier: number;
}

export interface YieldInfo {
daily: number;
weekly: number;
monthly: number;
yearly: number;
apy: number;
}

export interface RiskMetrics {
diversificationScore: number;
concentrationRisk: number;
impermanentLossRisk: number;
smartContractRisk: number;
overallRisk: number;
}

export interface RebalancingSuggestion {
type: 'diversify' | 'harvest' | 'rebalance' | 'optimize';
priority: 'low' | 'medium' | 'high';
title: string;
description: string;
impact: string;
estimatedGain: string;
}

Web API Server

// src/server/app.ts
import express from 'express';
import cors from 'cors';
import { Connection } from '@solana/web3.js';
import { PortfolioAnalyzer } from '../analytics/PortfolioAnalyzer';
import cron from 'node-cron';

const app = express();
const PORT = process.env.PORT || 3001;

// Middleware
app.use(cors());
app.use(express.json());

// In-memory cache for portfolio data
const portfolioCache = new Map();

// RPC connection
const connection = new Connection(
process.env.RPC_URL || 'https://api.mainnet-beta.solana.com',
'confirmed'
);

// API Routes
app.get('/api/portfolio/:walletAddress', async (req, res) => {
try {
const { walletAddress } = req.params;
const { refresh } = req.query;

// Check cache first (unless refresh is requested)
if (!refresh && portfolioCache.has(walletAddress)) {
const cached = portfolioCache.get(walletAddress);
const cacheAge = Date.now() - cached.timestamp;

// Return cached data if less than 5 minutes old
if (cacheAge < 5 * 60 * 1000) {
return res.json({
success: true,
data: cached.data,
cached: true,
cacheAge: Math.floor(cacheAge / 1000)
});
}
}

console.log(`📊 Analyzing portfolio for: ${walletAddress}`);

const analyzer = new PortfolioAnalyzer(connection, walletAddress);
const analysis = await analyzer.analyzeCompletePortfolio();

// Cache the result
portfolioCache.set(walletAddress, {
data: analysis,
timestamp: Date.now()
});

res.json({
success: true,
data: analysis,
cached: false
});
} catch (error) {
console.error('Portfolio analysis error:', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
});
}
});

app.get('/api/portfolio/:walletAddress/positions/:type', async (req, res) => {
try {
const { walletAddress, type } = req.params;

const analyzer = new PortfolioAnalyzer(connection, walletAddress);

let positions;
switch (type) {
case 'staking':
positions = await analyzer['getStakingPositions']();
break;
case 'farming':
positions = await analyzer['getFarmingPositions']();
break;
case 'liquidity':
positions = await analyzer['getLiquidityPositions']();
break;
case 'dlmm':
positions = await analyzer['getDLMMPositions']();
break;
default:
return res.status(400).json({
success: false,
error: 'Invalid position type'
});
}

res.json({
success: true,
data: positions
});
} catch (error) {
console.error(`${type} positions error:`, error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
});
}
});

app.get('/api/portfolio/:walletAddress/yield-history', async (req, res) => {
try {
const { walletAddress } = req.params;
const { days = '30' } = req.query;

// In production, this would fetch from a database
const mockYieldHistory = generateMockYieldHistory(parseInt(days as string));

res.json({
success: true,
data: mockYieldHistory
});
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
});
}
});

// Health check
app.get('/api/health', (req, res) => {
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime()
});
});

// Scheduled tasks
cron.schedule('*/5 * * * *', () => {
console.log('🔄 Clearing portfolio cache...');
// Clear cache entries older than 30 minutes
const now = Date.now();
for (const [wallet, data] of portfolioCache.entries()) {
if (now - data.timestamp > 30 * 60 * 1000) {
portfolioCache.delete(wallet);
}
}
});

function generateMockYieldHistory(days: number) {
const history = [];
const now = Date.now();

for (let i = days; i >= 0; i--) {
const date = new Date(now - (i * 24 * 60 * 60 * 1000));
const baseYield = 50 + Math.sin(i * 0.1) * 10;
const noise = (Math.random() - 0.5) * 5;

history.push({
date: date.toISOString().split('T')[0],
dailyYield: baseYield + noise,
cumulativeYield: baseYield * (days - i),
apy: 15 + Math.sin(i * 0.05) * 3
});
}

return history;
}

app.listen(PORT, () => {
console.log(`🚀 Saros Analytics API server running on port ${PORT}`);
});

Frontend Dashboard (Next.js)

// pages/dashboard/[wallet].tsx
import { GetServerSideProps } from 'next';
import { useState, useEffect } from 'react';
import { Line, Doughnut, Bar } from 'react-chartjs-2';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
ArcElement,
BarElement,
} from 'chart.js';

ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
ArcElement,
BarElement
);

interface DashboardProps {
walletAddress: string;
}

export default function PortfolioDashboard({ walletAddress }: DashboardProps) {
const [portfolio, setPortfolio] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [refreshing, setRefreshing] = useState(false);

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

const fetchPortfolio = async (refresh = false) => {
try {
if (refresh) setRefreshing(true);
else setLoading(true);

const response = await fetch(
`/api/portfolio/${walletAddress}${refresh ? '?refresh=true' : ''}`
);

if (!response.ok) {
throw new Error('Failed to fetch portfolio data');
}

const result = await response.json();
if (result.success) {
setPortfolio(result.data);
setError(null);
} else {
throw new Error(result.error);
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
setRefreshing(false);
}
};

if (loading) {
return (
<div className="min-h-screen bg-gray-900 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-500 mx-auto"></div>
<p className="text-white mt-4">Analyzing your portfolio...</p>
</div>
</div>
);
}

if (error) {
return (
<div className="min-h-screen bg-gray-900 flex items-center justify-center">
<div className="text-center">
<div className="text-red-500 text-xl mb-4">Error: {error}</div>
<button
onClick={() => fetchPortfolio()}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Retry
</button>
</div>
</div>
);
}

const portfolioBreakdownData = {
labels: ['Staking', 'Farming', 'Liquidity', 'DLMM', 'Tokens'],
datasets: [
{
data: [
portfolio.breakdown.staking.value,
portfolio.breakdown.farming.value,
portfolio.breakdown.liquidity.value,
portfolio.breakdown.dlmm.value,
portfolio.breakdown.tokens.value,
],
backgroundColor: [
'#8B5CF6',
'#06B6D4',
'#10B981',
'#F59E0B',
'#EF4444',
],
borderWidth: 2,
borderColor: '#1F2937',
},
],
};

return (
<div className="min-h-screen bg-gray-900 text-white">
{/* Header */}
<div className="bg-gray-800 border-b border-gray-700">
<div className="max-w-7xl mx-auto px-4 py-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold">Portfolio Dashboard</h1>
<p className="text-gray-400 mt-1">
{walletAddress.slice(0, 8)}...{walletAddress.slice(-8)}
</p>
</div>
<button
onClick={() => fetchPortfolio(true)}
disabled={refreshing}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
>
{refreshing ? 'Refreshing...' : 'Refresh'}
</button>
</div>
</div>
</div>

<div className="max-w-7xl mx-auto px-4 py-8">
{/* Key Metrics */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<MetricCard
title="Total Portfolio Value"
value={`$${portfolio.totalValue.toLocaleString()}`}
icon="💰"
/>
<MetricCard
title="Total Daily Yield"
value={`$${portfolio.yields.total.daily.toFixed(2)}`}
icon="📈"
subtitle={`${portfolio.yields.total.apy.toFixed(2)}% APY`}
/>
<MetricCard
title="Positions"
value={`${
portfolio.positions.staking.length +
portfolio.positions.farming.length +
portfolio.positions.liquidity.length +
portfolio.positions.dlmm.length
}`}
icon="📊"
/>
<MetricCard
title="Risk Score"
value={`${portfolio.riskMetrics.overallRisk.toFixed(0)}/100`}
icon="⚠️"
color={portfolio.riskMetrics.overallRisk > 50 ? 'text-red-400' : 'text-green-400'}
/>
</div>

{/* Charts */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
{/* Portfolio Breakdown */}
<div className="bg-gray-800 p-6 rounded-lg">
<h3 className="text-xl font-semibold mb-4">Portfolio Breakdown</h3>
<div className="h-64">
<Doughnut
data={portfolioBreakdownData}
options={{
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: {
color: 'white'
}
}
}
}}
/>
</div>
</div>

{/* Yield Performance */}
<div className="bg-gray-800 p-6 rounded-lg">
<h3 className="text-xl font-semibold mb-4">Yield by Category</h3>
<YieldChart yields={portfolio.yields} />
</div>
</div>

{/* Positions Table */}
<div className="bg-gray-800 p-6 rounded-lg mb-8">
<h3 className="text-xl font-semibold mb-4">Active Positions</h3>
<PositionsTable positions={portfolio.positions} />
</div>

{/* Risk Analysis & Suggestions */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<RiskAnalysis riskMetrics={portfolio.riskMetrics} />
<RebalancingSuggestions suggestions={portfolio.rebalancingSuggestions} />
</div>
</div>
</div>
);
}

// Components
function MetricCard({ title, value, icon, subtitle, color = 'text-white' }: any) {
return (
<div className="bg-gray-800 p-6 rounded-lg">
<div className="flex items-center justify-between">
<div>
<p className="text-gray-400 text-sm">{title}</p>
<p className={`text-2xl font-bold ${color}`}>{value}</p>
{subtitle && <p className="text-gray-400 text-xs mt-1">{subtitle}</p>}
</div>
<div className="text-2xl">{icon}</div>
</div>
</div>
);
}

function YieldChart({ yields }: any) {
const data = {
labels: ['Staking', 'Farming', 'Liquidity', 'DLMM'],
datasets: [
{
label: 'Daily Yield ($)',
data: [
yields.staking.daily,
yields.farming.daily,
yields.liquidity.daily,
yields.dlmm.daily,
],
backgroundColor: ['#8B5CF6', '#06B6D4', '#10B981', '#F59E0B'],
},
],
};

return (
<div className="h-48">
<Bar
data={data}
options={{
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
},
scales: {
x: {
ticks: { color: 'white' },
grid: { color: 'rgba(255,255,255,0.1)' },
},
y: {
ticks: { color: 'white' },
grid: { color: 'rgba(255,255,255,0.1)' },
},
},
}}
/>
</div>
);
}

function PositionsTable({ positions }: any) {
const allPositions = [
...positions.staking.map((p: any) => ({ ...p, type: 'Staking' })),
...positions.farming.map((p: any) => ({ ...p, type: 'Farming' })),
...positions.liquidity.map((p: any) => ({ ...p, type: 'Liquidity' })),
...positions.dlmm.map((p: any) => ({ ...p, type: 'DLMM' })),
].sort((a, b) => (b.totalValue || b.stakedValue || b.depositedValue) - (a.totalValue || a.stakedValue || a.depositedValue));

return (
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-gray-700">
<th className="text-left py-2">Type</th>
<th className="text-left py-2">Pool</th>
<th className="text-right py-2">Value</th>
<th className="text-right py-2">APY</th>
<th className="text-right py-2">Daily Yield</th>
</tr>
</thead>
<tbody>
{allPositions.slice(0, 10).map((position, index) => (
<tr key={index} className="border-b border-gray-800">
<td className="py-3">
<span className={`px-2 py-1 rounded text-xs ${getTypeColor(position.type)}`}>
{position.type}
</span>
</td>
<td className="py-3">
<div className="text-sm">
{position.stakingToken && `${position.stakingToken} Staking`}
{position.lpTokenMint && `LP Farming`}
{position.tokenA && `${position.tokenA.symbol}/${position.tokenB.symbol}`}
</div>
<div className="text-xs text-gray-400">
{(position.poolAddress || '').slice(0, 8)}...
</div>
</td>
<td className="text-right py-3">
${(position.totalValue || position.stakedValue || position.depositedValue || 0).toFixed(2)}
</td>
<td className="text-right py-3">
{position.apy ? `${position.apy.toFixed(1)}%` : 'N/A'}
</td>
<td className="text-right py-3">
${((position.totalValue || position.stakedValue || position.depositedValue || 0) * (position.apy || 0) / 100 / 365).toFixed(2)}
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

function getTypeColor(type: string) {
const colors = {
'Staking': 'bg-purple-900 text-purple-200',
'Farming': 'bg-cyan-900 text-cyan-200',
'Liquidity': 'bg-green-900 text-green-200',
'DLMM': 'bg-yellow-900 text-yellow-200',
};
return colors[type as keyof typeof colors] || 'bg-gray-900 text-gray-200';
}

function RiskAnalysis({ riskMetrics }: any) {
return (
<div className="bg-gray-800 p-6 rounded-lg">
<h3 className="text-xl font-semibold mb-4">Risk Analysis</h3>
<div className="space-y-4">
<RiskMeter label="Diversification" value={riskMetrics.diversificationScore} isGood={true} />
<RiskMeter label="Concentration Risk" value={riskMetrics.concentrationRisk} />
<RiskMeter label="IL Risk" value={riskMetrics.impermanentLossRisk} />
<RiskMeter label="Smart Contract Risk" value={riskMetrics.smartContractRisk} />
<div className="border-t border-gray-700 pt-4">
<RiskMeter
label="Overall Risk"
value={riskMetrics.overallRisk}
size="large"
/>
</div>
</div>
</div>
);
}

function RiskMeter({ label, value, isGood = false, size = 'normal' }: any) {
const getColor = () => {
if (isGood) {
return value > 70 ? 'bg-green-500' : value > 40 ? 'bg-yellow-500' : 'bg-red-500';
}
return value < 30 ? 'bg-green-500' : value < 60 ? 'bg-yellow-500' : 'bg-red-500';
};

return (
<div>
<div className="flex justify-between mb-2">
<span className={size === 'large' ? 'font-semibold' : ''}>{label}</span>
<span className={size === 'large' ? 'font-semibold' : ''}>{value.toFixed(0)}{size === 'large' ? '/100' : '%'}</span>
</div>
<div className="w-full bg-gray-700 rounded-full h-2">
<div
className={`${getColor()} h-2 rounded-full transition-all duration-300`}
style={{ width: `${Math.min(value, 100)}%` }}
></div>
</div>
</div>
);
}

function RebalancingSuggestions({ suggestions }: any) {
return (
<div className="bg-gray-800 p-6 rounded-lg">
<h3 className="text-xl font-semibold mb-4">Rebalancing Suggestions</h3>
<div className="space-y-4">
{suggestions.length === 0 ? (
<p className="text-gray-400">No rebalancing suggestions at this time. Your portfolio is well optimized!</p>
) : (
suggestions.map((suggestion: any, index: number) => (
<SuggestionCard key={index} suggestion={suggestion} />
))
)}
</div>
</div>
);
}

function SuggestionCard({ suggestion }: any) {
const priorityColors = {
low: 'border-gray-600 text-gray-300',
medium: 'border-yellow-600 text-yellow-300',
high: 'border-red-600 text-red-300',
};

return (
<div className={`border-l-4 ${priorityColors[suggestion.priority as keyof typeof priorityColors]} pl-4 py-2`}>
<div className="flex justify-between items-start mb-2">
<h4 className="font-semibold">{suggestion.title}</h4>
<span className={`px-2 py-1 rounded text-xs ${priorityColors[suggestion.priority as keyof typeof priorityColors]} border`}>
{suggestion.priority}
</span>
</div>
<p className="text-gray-400 text-sm mb-2">{suggestion.description}</p>
<div className="text-sm">
<div className="text-blue-400">Impact: {suggestion.impact}</div>
<div className="text-green-400">Potential Gain: {suggestion.estimatedGain}</div>
</div>
</div>
);
}

export const getServerSideProps: GetServerSideProps = async (context) => {
const { wallet } = context.params!;

return {
props: {
walletAddress: wallet as string,
},
};
};

Testing Suite

// tests/analytics.test.ts
import { PortfolioAnalyzer } from '../src/analytics/PortfolioAnalyzer';
import { Connection } from '@solana/web3.js';

describe('PortfolioAnalyzer', () => {
let analyzer: PortfolioAnalyzer;
const connection = new Connection('https://api.devnet.solana.com');
const testWallet = 'TestWalletAddressHere';

beforeEach(() => {
analyzer = new PortfolioAnalyzer(connection, testWallet);
});

test('should calculate portfolio breakdown correctly', async () => {
const analysis = await analyzer.analyzeCompletePortfolio();

expect(analysis.totalValue).toBeGreaterThanOrEqual(0);
expect(analysis.breakdown.staking.percentage).toBeGreaterThanOrEqual(0);
expect(analysis.breakdown.staking.percentage).toBeLessThanOrEqual(100);

// All percentages should sum to 100
const totalPercentage = Object.values(analysis.breakdown)
.reduce((sum, category) => sum + category.percentage, 0);
expect(Math.abs(totalPercentage - 100)).toBeLessThan(0.01);
});

test('should handle empty portfolio', async () => {
const emptyWallet = 'EmptyWalletAddress';
const emptyAnalyzer = new PortfolioAnalyzer(connection, emptyWallet);

const analysis = await emptyAnalyzer.analyzeCompletePortfolio();

expect(analysis.totalValue).toBe(0);
expect(analysis.positions.staking).toHaveLength(0);
expect(analysis.positions.farming).toHaveLength(0);
});

test('should calculate risk metrics', async () => {
const analysis = await analyzer.analyzeCompletePortfolio();

expect(analysis.riskMetrics.diversificationScore).toBeGreaterThanOrEqual(0);
expect(analysis.riskMetrics.diversificationScore).toBeLessThanOrEqual(100);
expect(analysis.riskMetrics.overallRisk).toBeGreaterThanOrEqual(0);
expect(analysis.riskMetrics.overallRisk).toBeLessThanOrEqual(100);
});
});

// Performance tests
describe('PortfolioAnalyzer Performance', () => {
test('should complete analysis within reasonable time', async () => {
const start = Date.now();
const analyzer = new PortfolioAnalyzer(
new Connection('https://api.devnet.solana.com'),
'TestWallet'
);

await analyzer.analyzeCompletePortfolio();

const duration = Date.now() - start;
expect(duration).toBeLessThan(30000); // Less than 30 seconds
});
});

Deployment Configuration

// package.json
{
"name": "saros-analytics-dashboard",
"version": "1.0.0",
"scripts": {
"dev": "concurrently \"npm run server\" \"npm run next\"",
"server": "nodemon src/server/app.ts",
"next": "next dev",
"build": "tsc && next build",
"start": "npm run server:prod",
"server:prod": "node dist/server/app.js",
"test": "jest",
"test:watch": "jest --watch",
"docker:build": "docker build -t saros-analytics .",
"docker:run": "docker run -p 3000:3000 -p 3001:3001 saros-analytics"
}
}
# Dockerfile
FROM node:18-alpine

WORKDIR /app
COPY package*.json ./
RUN npm install

COPY . .
RUN npm run build

EXPOSE 3000 3001

CMD ["npm", "start"]

Environment Configuration

# .env.example
RPC_URL=https://api.mainnet-beta.solana.com
PORT=3001
NEXT_PUBLIC_API_URL=http://localhost:3001
CACHE_DURATION=300000
ENABLE_ANALYTICS=true

Usage Examples

Running the Dashboard

# Development mode
npm run dev

# Production deployment
npm run build
npm start

# Docker deployment
npm run docker:build
npm run docker:run

API Usage

# Get complete portfolio analysis
curl "http://localhost:3001/api/portfolio/YourWalletAddress"

# Get specific position types
curl "http://localhost:3001/api/portfolio/YourWalletAddress/positions/staking"

# Get yield history
curl "http://localhost:3001/api/portfolio/YourWalletAddress/yield-history?days=30"

Key Features Demonstrated

  1. Comprehensive Portfolio Tracking: All Saros positions in one view
  2. Real-time Analytics: Live yield calculations and performance metrics
  3. Risk Assessment: Advanced risk analysis with actionable insights
  4. Visual Interface: Rich charts and graphs for data visualization
  5. API Architecture: Scalable backend with caching and optimization
  6. Testing Coverage: Unit and integration tests for reliability

This analytics dashboard provides a complete solution for monitoring DeFi portfolios on Saros, demonstrating advanced SDK usage patterns and real-world application architecture.