import React, { useState, useEffect } from 'react'; import { TrendingUp, Droplet, DollarSign, Percent, Info, AlertTriangle, BarChart3, Clock } 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'; // 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 [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); // 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 (

Loading pool data...

); } if (error) { return ( {error} ); } if (!poolData) { return ( No pool data available ); } // 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 */}

Pool Dashboards

Live
{/* Pool Dashboard Title */}

{asset0Symbol}/{asset1Symbol} Pool Dashboard

Monitor liquidity pool metrics and your position

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

Total Liquidity

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

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

{/* Current Price */}

{asset0Symbol} Price

${currentPrice.toFixed(4)}

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

{/* APR */}

Estimated APR

{estimateAPR().toFixed(2)}%

From swap fees

{/* Constant Product */}

Constant (k)

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

x × y = k

Reserves Your Position IL Calculator {/* Reserves Tab */}

Pool Reserves

{asset0Symbol} Reserve

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

Asset 1

{asset1Symbol} Reserve

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

Asset 2

AMM Formula

Pool maintains constant product: x × y = k

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

{/* Your Position Tab */}

Your Liquidity Position

{!selectedAccount ? ( Connect wallet to view your position ) : !lpPosition ? (

No liquidity position found

) : (

LP Tokens

{lpPosition.lpTokenBalance.toFixed(4)}

Pool Share

{lpPosition.share.toFixed(4)}%

Your Position Value

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

Estimated Earnings (APR {estimateAPR().toFixed(2)}%)

Daily: ~{((lpPosition.asset0Amount * 2 * estimateAPR()) / 365 / 100).toFixed(4)} {asset0Symbol}
Monthly: ~{((lpPosition.asset0Amount * 2 * estimateAPR()) / 12 / 100).toFixed(4)} {asset0Symbol}
Yearly: ~{((lpPosition.asset0Amount * 2 * estimateAPR()) / 100).toFixed(4)} {asset0Symbol}
)}
{/* Impermanent Loss Calculator Tab */}

Impermanent Loss Calculator

If {asset0Symbol} price changes by:

{[10, 25, 50, 100, 200].map((change) => { const il = calculateImpermanentLoss(change); return (
+{change}% {il.toFixed(2)}% Loss
); })}

What is Impermanent Loss?

Impermanent loss occurs when the price ratio of tokens in the pool changes. The larger the price change, the greater the loss compared to simply holding the tokens. Fees earned from swaps can offset this loss over time.

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