import React, { useState, useEffect } from 'react'; import { TrendingUp, Droplet, DollarSign, Percent, Info, AlertTriangle, BarChart3, Clock, Lock } from 'lucide-react'; import { Card } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Badge } from '@/components/ui/badge'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { usePezkuwi } from '@/contexts/PezkuwiContext'; import { ASSET_IDS, getAssetSymbol } from '@pezkuwi/lib/wallet'; import { NATIVE_TOKEN_ID } from '@/types/dex'; import { AddLiquidityModal } from '@/components/AddLiquidityModal'; import { RemoveLiquidityModal } from '@/components/RemoveLiquidityModal'; import { LPStakingModal } from '@/components/LPStakingModal'; import { useTranslation } from 'react-i18next'; // Helper function to convert asset IDs to user-friendly display names // Users should only see HEZ, PEZ, USDT - wrapped tokens are backend details const getDisplayTokenName = (assetId: number): string => { if (assetId === -1) return 'HEZ'; // Native HEZ from relay chain if (assetId === ASSET_IDS.WHEZ || assetId === 2) return 'wHEZ'; if (assetId === ASSET_IDS.PEZ || assetId === 1) return 'PEZ'; if (assetId === ASSET_IDS.WUSDT || assetId === 1000) return 'USDT'; if (assetId === 1001) return 'DOT'; if (assetId === 1002) return 'ETH'; if (assetId === 1003) return 'BTC'; return getAssetSymbol(assetId); // Fallback for other assets }; interface PoolData { asset0: number; asset1: number; reserve0: number; reserve1: number; lpTokenId: number; poolAccount: string; } interface LPPosition { lpTokenBalance: number; share: number; // Percentage of pool asset0Amount: number; asset1Amount: number; } const PoolDashboard = () => { // Use Asset Hub API for DEX operations (assetConversion pallet is on Asset Hub) const { assetHubApi, isAssetHubReady, selectedAccount } = usePezkuwi(); const { t } = useTranslation(); const [poolData, setPoolData] = useState(null); const [lpPosition, setLPPosition] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [isAddLiquidityModalOpen, setIsAddLiquidityModalOpen] = useState(false); const [isRemoveLiquidityModalOpen, setIsRemoveLiquidityModalOpen] = useState(false); const [isStakingModalOpen, setIsStakingModalOpen] = useState(false); // Pool selection state const [availablePools, setAvailablePools] = useState>([]); const [selectedPool, setSelectedPool] = useState('0-1'); // Default: wHEZ/PEZ // Helper to convert asset ID to XCM Location format (same as CreatePoolModal) const formatAssetId = (id: number) => { if (id === NATIVE_TOKEN_ID) { // Native token from relay chain - XCM location format return { parents: 1, interior: 'Here' }; } // Asset on Asset Hub - XCM location format with PalletInstance 50 (assets pallet) return { parents: 0, interior: { X2: [{ PalletInstance: 50 }, { GeneralIndex: id }] } }; }; // Discover available pools useEffect(() => { if (!assetHubApi || !isAssetHubReady) return; const discoverPools = async () => { try { // All possible pool combinations const possiblePools: Array<[number, number]> = [ // Native HEZ pools [NATIVE_TOKEN_ID, ASSET_IDS.PEZ], // Native HEZ / PEZ [NATIVE_TOKEN_ID, ASSET_IDS.WUSDT], // Native HEZ / wUSDT [NATIVE_TOKEN_ID, ASSET_IDS.WHEZ], // Native HEZ / wHEZ [NATIVE_TOKEN_ID, 1001], // Native HEZ / wDOT // wHEZ pools [ASSET_IDS.WHEZ, ASSET_IDS.PEZ], // wHEZ / PEZ [ASSET_IDS.WHEZ, ASSET_IDS.WUSDT], // wHEZ / wUSDT // PEZ pools [ASSET_IDS.PEZ, ASSET_IDS.WUSDT], // PEZ / wUSDT ]; const existingPools: Array<[number, number]> = []; for (const [asset0, asset1] of possiblePools) { try { // Use XCM Location format for pool queries const poolKey = [formatAssetId(asset0), formatAssetId(asset1)]; const poolInfo = await assetHubApi.query.assetConversion.pools(poolKey); if ((poolInfo as { isSome: boolean }).isSome) { existingPools.push([asset0, asset1]); if (import.meta.env.DEV) { console.log(`✅ Found pool: ${asset0}-${asset1}`); } } } catch (err) { // Skip pools that error out (likely don't exist) if (import.meta.env.DEV) { console.log(`❌ Pool ${asset0}-${asset1} not found or error:`, err); } } } if (import.meta.env.DEV) { console.log('📊 Total pools found:', existingPools.length, existingPools); } setAvailablePools(existingPools); // Set default pool to first available if current selection doesn't exist if (existingPools.length > 0) { const currentPoolKey = selectedPool; const poolExists = existingPools.some( ([a0, a1]) => `${a0}-${a1}` === currentPoolKey ); if (!poolExists) { const [firstAsset0, firstAsset1] = existingPools[0]; setSelectedPool(`${firstAsset0}-${firstAsset1}`); } } } catch (err) { if (import.meta.env.DEV) console.error('Error discovering pools:', err); } }; discoverPools(); }, [assetHubApi, isAssetHubReady, selectedPool]); // Fetch pool data useEffect(() => { if (!assetHubApi || !isAssetHubReady || !selectedPool) return; const fetchPoolData = async () => { setIsLoading(true); setError(null); try { // Parse selected pool (e.g., "-1-1" -> [-1, 1] for Native HEZ / PEZ) const [asset1Str, asset2Str] = selectedPool.split('-').filter(s => s !== ''); const asset1 = selectedPool.startsWith('-') ? -parseInt(asset1Str) : parseInt(asset1Str); const asset2 = parseInt(asset2Str); // Use XCM Location format for pool query const poolKey = [formatAssetId(asset1), formatAssetId(asset2)]; const poolInfo = await assetHubApi.query.assetConversion.pools(poolKey); if ((poolInfo as { isSome: boolean }).isSome) { const lpTokenData = (poolInfo as { unwrap: () => { toJSON: () => Record } }).unwrap().toJSON(); const lpTokenId = lpTokenData.lpToken as number; // Get decimals for each asset const getAssetDecimals = (assetId: number): number => { if (assetId === ASSET_IDS.WUSDT || assetId === 1000) return 6; // wUSDT has 6 decimals if (assetId === 1001) return 10; // wDOT has 10 decimals if (assetId === 1002) return 18; // wETH has 18 decimals if (assetId === 1003) return 8; // wBTC has 8 decimals return 12; // Native, wHEZ, PEZ have 12 decimals }; const asset1Decimals = getAssetDecimals(asset1); const asset2Decimals = getAssetDecimals(asset2); // Use getReserves runtime API for accurate reserve values let reserve0 = 0; let reserve1 = 0; try { const reserves = await assetHubApi.call.assetConversionApi.getReserves( formatAssetId(asset1), formatAssetId(asset2) ); if (reserves && !(reserves as { isNone?: boolean }).isNone) { const [reserve0Raw, reserve1Raw] = (reserves as { unwrap: () => [{ toString: () => string }, { toString: () => string }] }).unwrap(); reserve0 = Number(reserve0Raw.toString()) / Math.pow(10, asset1Decimals); reserve1 = Number(reserve1Raw.toString()) / Math.pow(10, asset2Decimals); if (import.meta.env.DEV) { console.log('📊 Pool reserves:', { reserve0, reserve1 }); } } } catch (err) { if (import.meta.env.DEV) console.warn('Could not fetch reserves:', err); } const poolAccount = 'Pool Account (derived)'; setPoolData({ asset0: asset1, asset1: asset2, reserve0, reserve1, lpTokenId, poolAccount, }); // Get user's LP position if account connected if (selectedAccount) { await fetchLPPosition(lpTokenId, reserve0, reserve1); } } else { setError('Pool not found'); } } catch (err) { if (import.meta.env.DEV) console.error('Error fetching pool data:', err); setError(err instanceof Error ? err.message : 'Failed to fetch pool data'); } finally { setIsLoading(false); } }; const fetchLPPosition = async (lpTokenId: number, reserve0: number, reserve1: number) => { if (!assetHubApi || !selectedAccount) return; try { // LP tokens can be in either poolAssets or assets pallet - check both let userLpBalance = 0; let totalSupply = 0; // Try poolAssets pallet first (newer LP tokens) try { const poolLpBalance = await assetHubApi.query.poolAssets.account(lpTokenId, selectedAccount.address); if ((poolLpBalance as { isSome: boolean }).isSome) { const lpData = (poolLpBalance as { unwrap: () => { toJSON: () => Record } }).unwrap().toJSON(); userLpBalance += Number(lpData.balance) / 1e12; } const poolLpAsset = await assetHubApi.query.poolAssets.asset(lpTokenId); if ((poolLpAsset as { isSome: boolean }).isSome) { const assetInfo = (poolLpAsset as { unwrap: () => { toJSON: () => Record } }).unwrap().toJSON(); totalSupply += Number(assetInfo.supply) / 1e12; } } catch { if (import.meta.env.DEV) console.log('poolAssets not available for LP token', lpTokenId); } // Also check assets pallet (some LP tokens might be there) try { const assetsLpBalance = await assetHubApi.query.assets.account(lpTokenId, selectedAccount.address); if ((assetsLpBalance as { isSome: boolean }).isSome) { const lpData = (assetsLpBalance as { unwrap: () => { toJSON: () => Record } }).unwrap().toJSON(); userLpBalance += Number(lpData.balance) / 1e12; } const assetsLpAsset = await assetHubApi.query.assets.asset(lpTokenId); if ((assetsLpAsset as { isSome: boolean }).isSome) { const assetInfo = (assetsLpAsset as { unwrap: () => { toJSON: () => Record } }).unwrap().toJSON(); // Only add if not already counted from poolAssets if (totalSupply === 0) { totalSupply = Number(assetInfo.supply) / 1e12; } } } catch { if (import.meta.env.DEV) console.log('assets pallet LP check failed for', lpTokenId); } if (userLpBalance > 0) { // Calculate user's share const sharePercentage = totalSupply > 0 ? (userLpBalance / totalSupply) * 100 : 0; // Calculate user's actual token amounts const asset0Amount = (sharePercentage / 100) * reserve0; const asset1Amount = (sharePercentage / 100) * reserve1; setLPPosition({ lpTokenBalance: userLpBalance, share: sharePercentage, asset0Amount, asset1Amount, }); if (import.meta.env.DEV) { console.log('📊 LP Position:', { userLpBalance, totalSupply, sharePercentage }); } } else { setLPPosition(null); } } catch (err) { if (import.meta.env.DEV) console.error('Error fetching LP position:', err); } }; fetchPoolData(); // Refresh every 30 seconds const interval = setInterval(fetchPoolData, 30000); return () => clearInterval(interval); }, [assetHubApi, isAssetHubReady, selectedAccount, selectedPool]); // Calculate metrics const constantProduct = poolData ? poolData.reserve0 * poolData.reserve1 : 0; const currentPrice = poolData ? poolData.reserve1 / poolData.reserve0 : 0; const totalLiquidityUSD = poolData ? poolData.reserve0 * 2 : 0; // Simplified: assumes 1:1 USD peg // APR calculation (simplified - would need 24h volume data) const estimateAPR = () => { if (!poolData) return 0; // Estimate based on pool size and typical volume // This is a simplified calculation // Real APR = (24h fees × 365) / TVL const dailyVolumeEstimate = totalLiquidityUSD * 0.1; // Assume 10% daily turnover const dailyFees = dailyVolumeEstimate * 0.003; // 0.3% fee const annualFees = dailyFees * 365; const apr = (annualFees / totalLiquidityUSD) * 100; return apr; }; // Impermanent loss calculator const calculateImpermanentLoss = (priceChange: number) => { // IL formula: 2 * sqrt(price_ratio) / (1 + price_ratio) - 1 const priceRatio = 1 + priceChange / 100; const il = ((2 * Math.sqrt(priceRatio)) / (1 + priceRatio) - 1) * 100; return il; }; if (isLoading && !poolData) { return (

{t('poolDash.loadingPoolData')}

); } if (error) { return ( {error} ); } if (!poolData) { return ( {t('poolDash.noPoolData')} ); } // Get asset symbols for the selected pool (using display names) const asset0Symbol = poolData ? getDisplayTokenName(poolData.asset0) : ''; const asset1Symbol = poolData ? getDisplayTokenName(poolData.asset1) : ''; return (
{/* Pool Selector */}

{t('poolDash.poolDashboards')}

{t('poolDash.live')}
{/* Pool Dashboard Title */}

{t('poolDash.poolDashboard', { asset0: asset0Symbol, asset1: asset1Symbol })}

{t('poolDash.monitorDesc')}

{/* Key Metrics Grid */}
{/* Total Liquidity */}

{t('poolDash.totalLiquidity')}

${totalLiquidityUSD.toLocaleString('en-US', { maximumFractionDigits: 0 })}

{poolData.reserve0.toLocaleString()} {asset0Symbol} + {poolData.reserve1.toLocaleString()} {asset1Symbol}

{/* Current Price */}

{t('poolDash.price', { symbol: asset0Symbol })}

${currentPrice.toFixed(4)}

1 {asset1Symbol} = {(1 / currentPrice).toFixed(4)} {asset0Symbol}

{/* APR */}

{t('poolDash.estimatedApr')}

{estimateAPR().toFixed(2)}%

{t('poolDash.fromSwapFees')}

{/* Constant Product */}

{t('poolDash.constant')}

{(constantProduct / 1e9).toFixed(1)}B

x × y = k

{t('poolDash.reserves')} {t('poolDash.yourPosition')} {t('poolDash.ilCalculator')} {/* Reserves Tab */}

{t('poolDash.poolReserves')}

{t('poolDash.reserve', { symbol: asset0Symbol })}

{poolData.reserve0.toLocaleString('en-US', { maximumFractionDigits: 2 })}

Asset 1

{t('poolDash.reserve', { symbol: asset1Symbol })}

{poolData.reserve1.toLocaleString('en-US', { maximumFractionDigits: 2 })}

Asset 2

{t('poolDash.ammFormula')}

{t('poolDash.poolMaintains')}

{poolData.reserve0.toFixed(2)} × {poolData.reserve1.toFixed(2)} = {constantProduct.toLocaleString()}

{/* Your Position Tab */}

{t('poolDash.yourLpPosition')}

{!selectedAccount ? ( {t('poolDash.connectWallet')} ) : !lpPosition ? (

{t('poolDash.noPosition')}

) : (

{t('poolDash.lpTokens')}

{lpPosition.lpTokenBalance.toFixed(4)}

{t('poolDash.poolShare')}

{lpPosition.share.toFixed(4)}%

{t('poolDash.positionValue')}

{asset0Symbol}: {lpPosition.asset0Amount.toFixed(4)}
{asset1Symbol}: {lpPosition.asset1Amount.toFixed(4)}

{t('poolDash.estimatedEarnings', { apr: estimateAPR().toFixed(2) })}

{t('poolDash.daily')} ~{((lpPosition.asset0Amount * 2 * estimateAPR()) / 365 / 100).toFixed(4)} {asset0Symbol}
{t('poolDash.monthly')} ~{((lpPosition.asset0Amount * 2 * estimateAPR()) / 12 / 100).toFixed(4)} {asset0Symbol}
{t('poolDash.yearly')} ~{((lpPosition.asset0Amount * 2 * estimateAPR()) / 100).toFixed(4)} {asset0Symbol}
)}
{/* Impermanent Loss Calculator Tab */}

{t('poolDash.ilCalcTitle')}

{t('poolDash.ifPriceChanges', { symbol: asset0Symbol })}

{[10, 25, 50, 100, 200].map((change) => { const il = calculateImpermanentLoss(change); return (
+{change}% {t('poolDash.loss', { percent: il.toFixed(2) })}
); })}

{t('poolDash.whatIsIL')}

{t('poolDash.ilExplanation')}

{/* Modals */} setIsAddLiquidityModalOpen(false)} asset0={poolData?.asset0} asset1={poolData?.asset1} /> {lpPosition && poolData && ( setIsRemoveLiquidityModalOpen(false)} lpPosition={lpPosition} lpTokenId={poolData.lpTokenId} asset0={poolData.asset0} asset1={poolData.asset1} /> )} setIsStakingModalOpen(false)} />
); }; export default PoolDashboard;