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 { usePolkadot } from '@/contexts/PolkadotContext'; import { useWallet } from '@/contexts/WalletContext'; import { ASSET_IDS, getAssetSymbol } from '@pezkuwi/lib/wallet'; 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 === ASSET_IDS.WHEZ || assetId === 0) return 'HEZ'; if (assetId === ASSET_IDS.PEZ || assetId === 1) return 'PEZ'; if (assetId === ASSET_IDS.WUSDT || assetId === 2) return 'USDT'; return getAssetSymbol(assetId); // Fallback for other assets }; // Helper function to get decimals for each asset const getAssetDecimals = (assetId: number): number => { if (assetId === ASSET_IDS.WUSDT) return 6; // wUSDT has 6 decimals return 12; // wHEZ, PEZ have 12 decimals }; 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 = () => { const { api, isApiReady, selectedAccount } = usePolkadot(); const { balances } = useWallet(); const [poolData, setPoolData] = useState(null); const [lpPosition, setLPPosition] = useState(null); const [isLoading, setIsLoading] = useState(false); 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('1-2'); // Default: PEZ/wUSDT // Discover available pools useEffect(() => { if (!api || !isApiReady) return; const discoverPools = async () => { try { // Check all possible pool combinations const possiblePools: Array<[number, number]> = [ [ASSET_IDS.WHEZ, ASSET_IDS.PEZ], // wHEZ/PEZ [ASSET_IDS.WHEZ, ASSET_IDS.WUSDT], // wHEZ/wUSDT [ASSET_IDS.PEZ, ASSET_IDS.WUSDT], // PEZ/wUSDT ]; const existingPools: Array<[number, number]> = []; for (const [asset0, asset1] of possiblePools) { const poolInfo = await api.query.assetConversion.pools([asset0, asset1]); if (poolInfo.isSome) { existingPools.push([asset0, asset1]); } } 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) { console.error('Error discovering pools:', err); } }; discoverPools(); }, [api, isApiReady]); // Fetch pool data useEffect(() => { if (!api || !isApiReady || !selectedPool) return; const fetchPoolData = async () => { setIsLoading(true); setError(null); try { // Parse selected pool (e.g., "1-2" -> [1, 2]) const [asset1Str, asset2Str] = selectedPool.split('-'); const asset1 = parseInt(asset1Str); const asset2 = parseInt(asset2Str); const poolId = [asset1, asset2]; const poolInfo = await api.query.assetConversion.pools(poolId); if (poolInfo.isSome) { const lpTokenData = poolInfo.unwrap().toJSON() as any; const lpTokenId = lpTokenData.lpToken; // Derive pool account using AccountIdConverter const { stringToU8a } = await import('@polkadot/util'); const { blake2AsU8a } = await import('@polkadot/util-crypto'); // PalletId for AssetConversion: "py/ascon" (8 bytes) const PALLET_ID = stringToU8a('py/ascon'); // Create PoolId tuple (u32, u32) const poolIdType = api.createType('(u32, u32)', [asset1, asset2]); // Create (PalletId, PoolId) tuple: ([u8; 8], (u32, u32)) const palletIdType = api.createType('[u8; 8]', PALLET_ID); const fullTuple = api.createType('([u8; 8], (u32, u32))', [palletIdType, poolIdType]); // Hash the SCALE-encoded tuple const accountHash = blake2AsU8a(fullTuple.toU8a(), 256); const poolAccountId = api.createType('AccountId32', accountHash); const poolAccount = poolAccountId.toString(); // Get reserves const asset0BalanceData = await api.query.assets.account(asset1, poolAccountId); const asset1BalanceData = await api.query.assets.account(asset2, poolAccountId); let reserve0 = 0; let reserve1 = 0; // Use dynamic decimals for each asset const asset1Decimals = getAssetDecimals(asset1); const asset2Decimals = getAssetDecimals(asset2); if (asset0BalanceData.isSome) { const asset0Data = asset0BalanceData.unwrap().toJSON() as any; reserve0 = Number(asset0Data.balance) / Math.pow(10, asset1Decimals); } if (asset1BalanceData.isSome) { const asset1Data = asset1BalanceData.unwrap().toJSON() as any; reserve1 = Number(asset1Data.balance) / Math.pow(10, asset2Decimals); } 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) { 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 (!api || !selectedAccount) return; try { // Query user's LP token balance const lpBalance = await api.query.poolAssets.account(lpTokenId, selectedAccount.address); if (lpBalance.isSome) { const lpData = lpBalance.unwrap().toJSON() as any; const userLpBalance = Number(lpData.balance) / 1e12; // Query total LP supply const lpAssetData = await api.query.poolAssets.asset(lpTokenId); if (lpAssetData.isSome) { const assetInfo = lpAssetData.unwrap().toJSON() as any; const totalSupply = Number(assetInfo.supply) / 1e12; // Calculate user's share const sharePercentage = (userLpBalance / totalSupply) * 100; // Calculate user's actual token amounts const asset0Amount = (sharePercentage / 100) * reserve0; const asset1Amount = (sharePercentage / 100) * reserve1; setLPPosition({ lpTokenBalance: userLpBalance, share: sharePercentage, asset0Amount, asset1Amount, }); } } } catch (err) { console.error('Error fetching LP position:', err); } }; fetchPoolData(); // Refresh every 30 seconds const interval = setInterval(fetchPoolData, 30000); return () => clearInterval(interval); }, [api, isApiReady, 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.03; // 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;