import React, { useState, useEffect } from 'react'; import { ArrowDownUp, Settings, TrendingUp, Clock, AlertCircle, Info, AlertTriangle } from 'lucide-react'; import { Card } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Badge } from '@/components/ui/badge'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { usePezkuwi } from '@/contexts/PezkuwiContext'; import { useWallet } from '@/contexts/WalletContext'; import { ASSET_IDS, formatBalance, parseAmount } from '@pezkuwi/lib/wallet'; import { formatAssetLocation, NATIVE_TOKEN_ID } from '@pezkuwi/utils/dex'; import { useToast } from '@/hooks/use-toast'; import { KurdistanSun } from './KurdistanSun'; import { PriceChart } from './trading/PriceChart'; import { useTranslation } from 'react-i18next'; // Available tokens for swap const AVAILABLE_TOKENS = [ { symbol: 'HEZ', emoji: '🟡', assetId: 0, name: 'HEZ', badge: true, displaySymbol: 'HEZ' }, { symbol: 'PEZ', emoji: '🟣', assetId: 1, name: 'PEZ', badge: true, displaySymbol: 'PEZ' }, { symbol: 'USDT', emoji: '💵', assetId: 1000, name: 'USDT', badge: true, displaySymbol: 'USDT' }, ] as const; const TokenSwap = () => { // Use Asset Hub API for DEX operations (assetConversion pallet is on Asset Hub) const { assetHubApi, isAssetHubReady, selectedAccount, walletSource } = usePezkuwi(); const { balances, refreshBalances } = useWallet(); const { toast } = useToast(); const { t } = useTranslation(); const [fromToken, setFromToken] = useState('PEZ'); const [toToken, setToToken] = useState('HEZ'); const [fromAmount, setFromAmount] = useState(''); const [slippage, setSlippage] = useState('0.5'); const [showSettings, setShowSettings] = useState(false); const [showConfirm, setShowConfirm] = useState(false); const [isSwapping, setIsSwapping] = useState(false); // DEX availability check const [isDexAvailable, setIsDexAvailable] = useState(false); // Exchange rate and loading states const [exchangeRate, setExchangeRate] = useState(0); const [isLoadingRate, setIsLoadingRate] = useState(false); // Get balances from wallet context if (import.meta.env.DEV) console.log('🔍 TokenSwap balances from context:', balances); if (import.meta.env.DEV) console.log('🔍 fromToken:', fromToken, 'toToken:', toToken); const fromBalance = balances[fromToken as keyof typeof balances]; const toBalance = balances[toToken as keyof typeof balances]; if (import.meta.env.DEV) console.log('🔍 Final balances:', { fromBalance, toBalance }); // Liquidity pool data interface LiquidityPool { key: string; data: unknown; tvl: number; } const [liquidityPools, setLiquidityPools] = useState([]); const [isLoadingPools, setIsLoadingPools] = useState(false); // Transaction history interface SwapTransaction { blockNumber: number; timestamp: number; from: string; fromToken: string; fromAmount: string; toToken: string; toAmount: string; txHash: string; } const [swapHistory, setSwapHistory] = useState([]); const [isLoadingHistory, setIsLoadingHistory] = useState(false); // Pool reserves for AMM calculation const [poolReserves, setPoolReserves] = useState<{ reserve0: number; reserve1: number; asset0: number; asset1: number; decimals0: number; decimals1: number; } | null>(null); // Helper: Get display name for token (USDT instead of wUSDT) const getTokenDisplayName = (tokenSymbol: string) => { const token = AVAILABLE_TOKENS.find(t => t.symbol === tokenSymbol); return token?.displaySymbol || tokenSymbol; }; // Check if user has insufficient balance const hasInsufficientBalance = React.useMemo(() => { const fromAmountNum = parseFloat(fromAmount || '0'); const fromBalanceNum = parseFloat(fromBalance?.toString() || '0'); return fromAmountNum > 0 && fromAmountNum > fromBalanceNum; }, [fromAmount, fromBalance]); // Calculate toAmount and price impact using AMM constant product formula const swapCalculations = React.useMemo(() => { if (!fromAmount || !poolReserves || parseFloat(fromAmount) <= 0) { return { toAmount: '', priceImpact: 0, minimumReceived: '', lpFee: '' }; } const amountIn = parseFloat(fromAmount); const { reserve0, reserve1, asset0, decimals0, decimals1 } = poolReserves; // Determine which reserve is input and which is output // Native HEZ uses NATIVE_TOKEN_ID (-1) const fromAssetId = fromToken === 'HEZ' ? NATIVE_TOKEN_ID : ASSET_IDS[fromToken as keyof typeof ASSET_IDS]; const isAsset0ToAsset1 = fromAssetId === asset0; const reserveIn = isAsset0ToAsset1 ? reserve0 : reserve1; const reserveOut = isAsset0ToAsset1 ? reserve1 : reserve0; const decimalsIn = isAsset0ToAsset1 ? decimals0 : decimals1; const decimalsOut = isAsset0ToAsset1 ? decimals1 : decimals0; // Uniswap V2 AMM formula (matches Substrate runtime exactly) // Runtime: amount_in_with_fee = amount_in * (1000 - LPFee) = amount_in * 997 // LPFee = 3 (0.3% fee - standard DEX fee) // Formula: amountOut = (amountIn * 997 * reserveOut) / (reserveIn * 1000 + amountIn * 997) const LP_FEE = 3; // 0.3% fee const amountInWithFee = amountIn * (1000 - LP_FEE); // = amountIn * 997 const numerator = amountInWithFee * reserveOut; const denominator = reserveIn * 1000 + amountInWithFee; const amountOut = numerator / denominator; // Calculate price impact (like Uniswap) // Price impact = (amount_in / reserve_in) / (1 + amount_in / reserve_in) * 100 const priceImpact = (amountIn / (reserveIn + amountIn)) * 100; // Calculate LP fee amount const lpFeeAmount = (amountIn * (LP_FEE / 1000)).toFixed(4); // Calculate minimum received with slippage // Use appropriate decimal places based on output token const outputDecimals = decimalsOut === 6 ? 2 : 4; // USDT: 2 decimals, others: 4 const minReceived = (amountOut * (1 - parseFloat(slippage) / 100)).toFixed(outputDecimals); if (import.meta.env.DEV) console.log('🔍 Uniswap V2 AMM:', { amountIn, amountInWithFee, reserveIn, reserveOut, decimalsIn, decimalsOut, numerator, denominator, amountOut, priceImpact: priceImpact.toFixed(2) + '%', lpFeeAmount, minReceived, feePercent: LP_FEE / 10 + '%' }); return { toAmount: amountOut.toFixed(outputDecimals), priceImpact, minimumReceived: minReceived, lpFee: lpFeeAmount }; }, [fromAmount, poolReserves, fromToken, slippage]); const { toAmount, priceImpact, minimumReceived, lpFee } = swapCalculations; // Check if AssetConversion pallet is available useEffect(() => { if (import.meta.env.DEV) console.log('🔍 Checking DEX availability...', { assetHubApi: !!assetHubApi, isAssetHubReady }); if (assetHubApi && isAssetHubReady) { const hasAssetConversion = assetHubApi.tx.assetConversion !== undefined; if (import.meta.env.DEV) console.log('🔍 AssetConversion pallet check:', hasAssetConversion); setIsDexAvailable(hasAssetConversion); if (!hasAssetConversion) { if (import.meta.env.DEV) console.warn('⚠️ AssetConversion pallet not available in runtime'); } else { if (import.meta.env.DEV) console.log('✅ AssetConversion pallet is available!'); } } }, [assetHubApi, isAssetHubReady]); // Fetch exchange rate from AssetConversion pool // Always use wHEZ/PEZ pool (the only valid pool) useEffect(() => { const fetchExchangeRate = async () => { if (import.meta.env.DEV) console.log('🔍 fetchExchangeRate check:', { assetHubApi: !!assetHubApi, isAssetHubReady, isDexAvailable, fromToken, toToken }); if (!assetHubApi || !isAssetHubReady || !isDexAvailable) { if (import.meta.env.DEV) console.log('⚠️ Skipping fetchExchangeRate:', { assetHubApi: !!assetHubApi, isAssetHubReady, isDexAvailable }); return; } if (import.meta.env.DEV) console.log('✅ Starting fetchExchangeRate...'); setIsLoadingRate(true); try { // Map user-selected tokens to actual pool assets // HEZ → Native token (NATIVE_TOKEN_ID = -1) const getPoolAssetId = (token: string) => { if (token === 'HEZ') return NATIVE_TOKEN_ID; // Native HEZ (-1) if (token === 'PEZ') return 1; if (token === 'USDT') return 1000; return ASSET_IDS[token as keyof typeof ASSET_IDS]; }; const fromAssetId = getPoolAssetId(fromToken); const toAssetId = getPoolAssetId(toToken); if (import.meta.env.DEV) console.log('🔍 Looking for pool:', { fromToken, toToken, fromAssetId, toAssetId }); // IMPORTANT: Pool ID must be sorted (native token first, then by asset ID) // Native token (-1) always comes first const [asset1, asset2] = fromAssetId === NATIVE_TOKEN_ID ? [fromAssetId, toAssetId] : toAssetId === NATIVE_TOKEN_ID ? [toAssetId, fromAssetId] : fromAssetId < toAssetId ? [fromAssetId, toAssetId] : [toAssetId, fromAssetId]; if (import.meta.env.DEV) console.log('🔍 Sorted pool assets:', { asset1, asset2 }); // Create pool asset tuple using XCM Location format // Native token: { parents: 1, interior: 'Here' } // Assets: { parents: 0, interior: { X2: [{ PalletInstance: 50 }, { GeneralIndex: id }] } } const poolAssets = [ formatAssetLocation(asset1), formatAssetLocation(asset2) ]; if (import.meta.env.DEV) console.log('🔍 Pool query with XCM Locations:', poolAssets); // Query pool from AssetConversion pallet const poolInfo = await assetHubApi.query.assetConversion.pools(poolAssets); if (import.meta.env.DEV) console.log('🔍 Pool query result:', poolInfo.toHuman()); if (import.meta.env.DEV) console.log('🔍 Pool isEmpty?', poolInfo.isEmpty, 'exists?', !poolInfo.isEmpty); if (poolInfo && !poolInfo.isEmpty) { const pool = poolInfo.toJSON() as Record; if (import.meta.env.DEV) console.log('🔍 Pool data:', pool); try { // Use Runtime API to get exchange rate (quotePriceExactTokensForTokens) // This is more reliable than deriving pool account manually // IMPORTANT: HEZ has 12 decimals, USDT has 6 decimals, PEZ has 12 decimals const decimals0 = asset1 === 1000 ? 6 : 12; // wUSDT: 6, others: 12 const decimals1 = asset2 === 1000 ? 6 : 12; const oneUnit = BigInt(Math.pow(10, decimals0)); if (import.meta.env.DEV) console.log('🔍 Querying price via runtime API...', { asset1, asset2, decimals0, decimals1 }); // eslint-disable-next-line @typescript-eslint/no-explicit-any const quote = await (assetHubApi.call as any).assetConversionApi.quotePriceExactTokensForTokens( formatAssetLocation(asset1), formatAssetLocation(asset2), oneUnit.toString(), true ); if (import.meta.env.DEV) console.log('🔍 Quote result:', quote?.toHuman?.() || quote); // eslint-disable-next-line @typescript-eslint/no-explicit-any if (quote && !(quote as any).isNone) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const priceRaw = (quote as any).unwrap().toString(); const price = Number(BigInt(priceRaw)) / Math.pow(10, decimals1); if (import.meta.env.DEV) console.log('✅ Price from runtime API:', price, { decimals0, decimals1, priceRaw }); // For AMM calculation, estimate reserves from LP supply const lpTokenId = pool.lpToken; let lpSupply = BigInt(1); if (assetHubApi.query.poolAssets?.asset) { const lpAssetDetails = await assetHubApi.query.poolAssets.asset(lpTokenId); // eslint-disable-next-line @typescript-eslint/no-explicit-any if ((lpAssetDetails as any).isSome) { // eslint-disable-next-line @typescript-eslint/no-explicit-any lpSupply = BigInt(((lpAssetDetails as any).unwrap() as any).supply.toString()); } } // Estimate reserves using correct decimals for each asset // LP = sqrt(r0_raw * r1_raw) where r0/r1 are in smallest units // // Mathematical derivation for mixed-decimal pools: // r0_raw = r0_human * 10^d0, r1_raw = r1_human * 10^d1 // LP_raw = sqrt(r0_human * 10^d0 * r1_human * 10^d1) // = sqrt(r0_human * r1_human) * 10^((d0+d1)/2) // // LP tokens have 12 decimals, so: // LP_human = LP_raw / 10^12 = sqrt(r0_human * r1_human) * 10^((d0+d1)/2 - 12) // // Therefore: sqrt(r0_human * r1_human) = LP_human * 10^(12 - (d0+d1)/2) // // For HEZ-USDT (d0=12, d1=6): factor = 10^(12 - 9) = 1000 // For HEZ-PEZ (d0=12, d1=12): factor = 10^(12 - 12) = 1 const lpHuman = Number(lpSupply) / Math.pow(10, 12); // LP tokens have 12 decimals const decimalCorrectionFactor = Math.pow(10, 12 - (decimals0 + decimals1) / 2); // Now estimate reserves: // sqrt(r0_human * r1_human) = LP_human * decimalCorrectionFactor // price = r1_human / r0_human // r0_human = sqrt(r0_human * r1_human) / sqrt(price) = LP_human * factor / sqrt(price) // r1_human = sqrt(r0_human * r1_human) * sqrt(price) = LP_human * factor * sqrt(price) const sqrtPrice = Math.sqrt(price); const reserve0 = lpHuman * decimalCorrectionFactor / sqrtPrice; const reserve1 = lpHuman * decimalCorrectionFactor * sqrtPrice; if (import.meta.env.DEV) console.log('✅ Estimated reserves (human units):', { reserve0, reserve1, price, lpSupply: lpSupply.toString(), lpHuman, decimals0, decimals1 }); // Store pool reserves for AMM calculation setPoolReserves({ reserve0, reserve1, asset0: asset1, asset1: asset2, decimals0, decimals1 }); // Calculate exchange rate based on direction const rate = fromAssetId === asset1 ? price : 1 / price; if (import.meta.env.DEV) console.log('✅ Exchange rate:', rate); setExchangeRate(rate); } else { if (import.meta.env.DEV) console.warn('⚠️ No price quote available'); setExchangeRate(0); } } catch (err) { if (import.meta.env.DEV) console.error('❌ Error fetching price:', err); setExchangeRate(0); } } else { if (import.meta.env.DEV) console.warn('No liquidity pool found for this pair'); setExchangeRate(0); } } catch (error) { if (import.meta.env.DEV) console.error('Failed to fetch exchange rate:', error); setExchangeRate(0); } finally { setIsLoadingRate(false); } }; fetchExchangeRate(); }, [assetHubApi, isAssetHubReady, isDexAvailable, fromToken, toToken]); // Fetch liquidity pools useEffect(() => { const fetchLiquidityPools = async () => { if (!assetHubApi || !isAssetHubReady || !isDexAvailable) { return; } setIsLoadingPools(true); try { // Query all pools from AssetConversion pallet const poolsEntries = await assetHubApi.query.assetConversion.pools.entries(); if (poolsEntries && poolsEntries.length > 0) { const pools = poolsEntries.map(([key, value]: [unknown, unknown]) => { const poolData = value.toJSON(); const poolKey = key.toHuman(); // Calculate TVL from reserves const tvl = poolData && poolData[0] && poolData[1] ? ((parseFloat(poolData[0]) + parseFloat(poolData[1])) / 1e12).toFixed(2) : '0'; // Parse asset IDs from pool key const assets = poolKey?.[0] || []; //const _asset1 = assets[0]?.NativeOrAsset?.Asset || '?'; const asset2 = assets[1]?.NativeOrAsset?.Asset || '?'; return { pool: `Asset ${asset1} / Asset ${asset2}`, tvl: `$${tvl}M`, apr: 'TBD', // Requires historical data volume: 'TBD', // Requires event indexing }; }); setLiquidityPools(pools.slice(0, 3)); } else { setLiquidityPools([]); } } catch (error) { if (import.meta.env.DEV) console.error('Failed to fetch liquidity pools:', error); setLiquidityPools([]); } finally { setIsLoadingPools(false); } }; fetchLiquidityPools(); }, [assetHubApi, isAssetHubReady, isDexAvailable]); // Fetch swap transaction history useEffect(() => { const fetchSwapHistory = async () => { if (!assetHubApi || !isAssetHubReady || !isDexAvailable || !selectedAccount) { return; } setIsLoadingHistory(true); try { // Get recent finalized blocks (last 100 blocks) const finalizedHead = await assetHubApi.rpc.chain.getFinalizedHead(); const finalizedBlock = await assetHubApi.rpc.chain.getBlock(finalizedHead); const currentBlockNumber = finalizedBlock.block.header.number.toNumber(); const startBlock = Math.max(0, currentBlockNumber - 100); if (import.meta.env.DEV) console.log('🔍 Fetching swap history from block', startBlock, 'to', currentBlockNumber); const transactions: SwapTransaction[] = []; // Query block by block for SwapExecuted events for (let blockNum = currentBlockNumber; blockNum >= startBlock && transactions.length < 10; blockNum--) { try { const blockHash = await assetHubApi.rpc.chain.getBlockHash(blockNum); const apiAt = await assetHubApi.at(blockHash); const events = await apiAt.query.system.events(); //const block = await assetHubApi.rpc.chain.getBlock(blockHash); const timestamp = Date.now() - ((currentBlockNumber - blockNum) * 6000); // Estimate 6s per block events.forEach((record: { event: { data: unknown[] } }) => { const { event } = record; // Check for AssetConversion::SwapExecuted event if (assetHubApi.events.assetConversion?.SwapExecuted?.is(event)) { // SwapExecuted has 5 fields: (who, send_to, amountIn, amountOut, path) const [who, , amountIn, amountOut, path] = event.data; // Parse path to get token symbols - path is Vec let fromAssetId = 0; let toAssetId = 0; try { // Path structure is: [[assetId, amount], [assetId, amount]] const pathArray = path.toJSON ? path.toJSON() : path; if (Array.isArray(pathArray) && pathArray.length >= 2) { // Extract asset IDs from path const asset0 = pathArray[0]; //const _asset1 = pathArray[1]; // Each element is a tuple where index 0 is the asset ID if (Array.isArray(asset0) && asset0.length >= 1) { fromAssetId = typeof asset0[0] === 'number' ? asset0[0] : parseInt(asset0[0]) || 0; } if (Array.isArray(asset1) && asset1.length >= 1) { toAssetId = typeof asset1[0] === 'number' ? asset1[0] : parseInt(asset1[0]) || 0; } } } catch (err) { if (import.meta.env.DEV) console.warn('Failed to parse swap path:', err); } const fromTokenSymbol = fromAssetId === 0 ? 'wHEZ' : fromAssetId === 1 ? 'PEZ' : fromAssetId === 1000 ? 'USDT' : `Asset${fromAssetId}`; const toTokenSymbol = toAssetId === 0 ? 'wHEZ' : toAssetId === 1 ? 'PEZ' : toAssetId === 1000 ? 'USDT' : `Asset${toAssetId}`; // Only show transactions from current user if (who.toString() === selectedAccount.address) { transactions.push({ blockNumber: blockNum, timestamp, from: who.toString(), fromToken: fromTokenSymbol === 'wHEZ' ? 'HEZ' : fromTokenSymbol, fromAmount: formatBalance(amountIn.toString()), toToken: toTokenSymbol === 'wHEZ' ? 'HEZ' : toTokenSymbol, toAmount: formatBalance(amountOut.toString()), txHash: blockHash.toHex() }); } } }); } catch (err) { if (import.meta.env.DEV) console.warn(`Failed to fetch block ${blockNum}:`, err); } } if (import.meta.env.DEV) console.log('✅ Swap history fetched:', transactions.length, 'transactions'); setSwapHistory(transactions.slice(0, 10)); // Show max 10 } catch (error) { if (import.meta.env.DEV) console.error('Failed to fetch swap history:', error); setSwapHistory([]); } finally { setIsLoadingHistory(false); } }; fetchSwapHistory(); }, [assetHubApi, isAssetHubReady, isDexAvailable, selectedAccount]); const handleSwap = () => { setFromToken(toToken); setToToken(fromToken); setFromAmount(''); }; const handleConfirmSwap = async () => { if (!assetHubApi || !selectedAccount) { toast({ title: t('common.error'), description: t('common.connectWalletAlert'), variant: 'destructive', }); return; } if (!isDexAvailable) { toast({ title: t('tokenSwap.dexNotAvailable'), description: t('tokenSwap.dexNotAvailableDesc'), variant: 'destructive', }); return; } if (!exchangeRate || exchangeRate === 0) { toast({ title: t('common.error'), description: t('swap.noPool'), variant: 'destructive', }); return; } // ✅ BALANCE VALIDATION - Check if user has sufficient balance const fromAmountNum = parseFloat(fromAmount); const fromBalanceNum = parseFloat(fromBalance?.toString() || '0'); if (fromAmountNum > fromBalanceNum) { toast({ title: t('swap.insufficientBalanceBtn', { token: getTokenDisplayName(fromToken) }), description: t('tokenSwap.insufficientBalanceToast', { balance: fromBalanceNum.toFixed(4), token: getTokenDisplayName(fromToken), amount: fromAmountNum }), variant: 'destructive', }); return; } setIsSwapping(true); setShowConfirm(false); // Close dialog before transaction starts try { // Get correct decimals for each token const getTokenDecimals = (token: string) => { if (token === 'USDT') return 6; // USDT has 6 decimals return 12; // HEZ, wHEZ, PEZ all have 12 decimals }; const fromDecimals = getTokenDecimals(fromToken); const toDecimals = getTokenDecimals(toToken); const amountIn = parseAmount(fromAmount, fromDecimals); const minAmountOut = parseAmount( (parseFloat(toAmount) * (1 - parseFloat(slippage) / 100)).toString(), toDecimals ); if (import.meta.env.DEV) console.log('💰 Swap amounts:', { fromToken, toToken, fromAmount, toAmount, fromDecimals, toDecimals, amountIn: amountIn.toString(), minAmountOut: minAmountOut.toString() }); // Get signer (extension or WalletConnect) const { getSigner } = await import('@/lib/get-signer'); const injector = await getSigner(selectedAccount.address, walletSource, assetHubApi); // Build transaction based on token types let tx; // Use XCM MultiLocation format for swap paths // Native HEZ: { parents: 1, interior: 'Here' } // Assets: { parents: 0, interior: { X2: [{ PalletInstance: 50 }, { GeneralIndex: id }] } } const nativeLocation = formatAssetLocation(NATIVE_TOKEN_ID); const pezLocation = formatAssetLocation(1); const usdtLocation = formatAssetLocation(1000); if (fromToken === 'HEZ' && toToken === 'PEZ') { // HEZ → PEZ: Direct swap using native token pool tx = assetHubApi.tx.assetConversion.swapExactTokensForTokens( [nativeLocation, pezLocation], amountIn.toString(), minAmountOut.toString(), selectedAccount.address, true ); } else if (fromToken === 'PEZ' && toToken === 'HEZ') { // PEZ → HEZ: Direct swap to native token tx = assetHubApi.tx.assetConversion.swapExactTokensForTokens( [pezLocation, nativeLocation], amountIn.toString(), minAmountOut.toString(), selectedAccount.address, true ); } else if (fromToken === 'HEZ' && toToken === 'USDT') { // HEZ → USDT: Direct swap using native token pool tx = assetHubApi.tx.assetConversion.swapExactTokensForTokens( [nativeLocation, usdtLocation], amountIn.toString(), minAmountOut.toString(), selectedAccount.address, true ); } else if (fromToken === 'USDT' && toToken === 'HEZ') { // USDT → HEZ: Direct swap to native token tx = assetHubApi.tx.assetConversion.swapExactTokensForTokens( [usdtLocation, nativeLocation], amountIn.toString(), minAmountOut.toString(), selectedAccount.address, true ); } else if (fromToken === 'PEZ' && toToken === 'USDT') { // PEZ → USDT: Multi-hop through HEZ (PEZ → HEZ → USDT) tx = assetHubApi.tx.assetConversion.swapExactTokensForTokens( [pezLocation, nativeLocation, usdtLocation], amountIn.toString(), minAmountOut.toString(), selectedAccount.address, true ); } else if (fromToken === 'USDT' && toToken === 'PEZ') { // USDT → PEZ: Multi-hop through HEZ (USDT → HEZ → PEZ) tx = assetHubApi.tx.assetConversion.swapExactTokensForTokens( [usdtLocation, nativeLocation, pezLocation], amountIn.toString(), minAmountOut.toString(), selectedAccount.address, true ); } else { // Generic swap using XCM Locations const getAssetLocation = (token: string) => { if (token === 'HEZ') return nativeLocation; if (token === 'PEZ') return pezLocation; if (token === 'USDT') return usdtLocation; const assetId = ASSET_IDS[token as keyof typeof ASSET_IDS]; return formatAssetLocation(assetId); }; tx = assetHubApi.tx.assetConversion.swapExactTokensForTokens( [getAssetLocation(fromToken), getAssetLocation(toToken)], amountIn.toString(), minAmountOut.toString(), selectedAccount.address, true ); } // Sign and send transaction await tx.signAndSend( selectedAccount.address, { signer: injector.signer }, async ({ status, events, dispatchError }) => { if (import.meta.env.DEV) console.log('🔍 Transaction status:', status.toHuman()); if (status.isInBlock) { if (import.meta.env.DEV) console.log('✅ Transaction in block:', status.asInBlock.toHex()); toast({ title: t('tokenSwap.txSubmitted'), description: t('tokenSwap.processingInBlock', { hash: status.asInBlock.toHex().slice(0, 10) }), }); } if (status.isFinalized) { if (import.meta.env.DEV) console.log('✅ Transaction finalized:', status.asFinalized.toHex()); if (import.meta.env.DEV) console.log('🔍 All events:', events.map(({ event }) => event.toHuman())); if (import.meta.env.DEV) console.log('🔍 dispatchError:', dispatchError?.toHuman()); // Check for errors if (dispatchError) { let errorMessage = 'Transaction failed'; if (dispatchError.isModule) { const decoded = assetHubApi.registry.findMetaError(dispatchError.asModule); errorMessage = `${decoded.section}.${decoded.name}: ${decoded.docs}`; } toast({ title: t('common.error'), description: errorMessage, variant: 'destructive', }); setIsSwapping(false); return; } // Success - check for swap event const hasSwapEvent = events.some(({ event }) => assetHubApi.events.assetConversion?.SwapExecuted?.is(event) ); if (hasSwapEvent || fromToken === 'HEZ' || toToken === 'HEZ') { toast({ title: t('common.success'), description: t('swap.swapped', { fromAmount, fromToken, toAmount, toToken }), }); setFromAmount(''); // Refresh balances and history without page reload await refreshBalances(); if (import.meta.env.DEV) console.log('✅ Balances refreshed after swap'); // Refresh swap history after 3 seconds (wait for block finalization) setTimeout(async () => { if (import.meta.env.DEV) console.log('🔄 Refreshing swap history...'); const fetchSwapHistory = async () => { if (!assetHubApi || !isAssetHubReady || !isDexAvailable || !selectedAccount) return; setIsLoadingHistory(true); try { const finalizedHead = await assetHubApi.rpc.chain.getFinalizedHead(); const finalizedBlock = await assetHubApi.rpc.chain.getBlock(finalizedHead); const currentBlockNumber = finalizedBlock.block.header.number.toNumber(); const startBlock = Math.max(0, currentBlockNumber - 100); const transactions: SwapTransaction[] = []; for (let blockNum = currentBlockNumber; blockNum >= startBlock && transactions.length < 10; blockNum--) { try { const blockHash = await assetHubApi.rpc.chain.getBlockHash(blockNum); const apiAt = await assetHubApi.at(blockHash); const events = await apiAt.query.system.events(); const timestamp = Date.now() - ((currentBlockNumber - blockNum) * 6000); events.forEach((record: { event: { data: unknown[] } }) => { const { event } = record; if (assetHubApi.events.assetConversion?.SwapExecuted?.is(event)) { // SwapExecuted has 5 fields: (who, send_to, amountIn, amountOut, path) const [who, , amountIn, amountOut, path] = event.data; // Parse path (same logic as main history fetch) let fromAssetId = 0; let toAssetId = 0; try { // Path structure is: [[assetId, amount], [assetId, amount]] const pathArray = path.toJSON ? path.toJSON() : path; if (Array.isArray(pathArray) && pathArray.length >= 2) { const asset0 = pathArray[0]; const asset1 = pathArray[1]; // Each element is a tuple where index 0 is the asset ID if (Array.isArray(asset0) && asset0.length >= 1) { fromAssetId = typeof asset0[0] === 'number' ? asset0[0] : parseInt(asset0[0]) || 0; } if (Array.isArray(asset1) && asset1.length >= 1) { toAssetId = typeof asset1[0] === 'number' ? asset1[0] : parseInt(asset1[0]) || 0; } } } catch (err) { if (import.meta.env.DEV) console.warn('Failed to parse swap path in refresh:', err); } const fromTokenSymbol = fromAssetId === 0 ? 'wHEZ' : fromAssetId === 1 ? 'PEZ' : fromAssetId === 1000 ? 'USDT' : `Asset${fromAssetId}`; const toTokenSymbol = toAssetId === 0 ? 'wHEZ' : toAssetId === 1 ? 'PEZ' : toAssetId === 1000 ? 'USDT' : `Asset${toAssetId}`; if (who.toString() === selectedAccount.address) { transactions.push({ blockNumber: blockNum, timestamp, from: who.toString(), fromToken: fromTokenSymbol === 'wHEZ' ? 'HEZ' : fromTokenSymbol, fromAmount: formatBalance(amountIn.toString()), toToken: toTokenSymbol === 'wHEZ' ? 'HEZ' : toTokenSymbol, toAmount: formatBalance(amountOut.toString()), txHash: blockHash.toHex() }); } } }); } catch (err) { if (import.meta.env.DEV) console.warn(`Failed to fetch block ${blockNum}:`, err); } } setSwapHistory(transactions.slice(0, 10)); } catch (error) { if (import.meta.env.DEV) console.error('Failed to refresh swap history:', error); } finally { setIsLoadingHistory(false); } }; await fetchSwapHistory(); }, 3000); } else { toast({ title: t('common.error'), description: t('swap.swapFailed'), variant: 'destructive', }); } setIsSwapping(false); } } ); } catch (error) { if (import.meta.env.DEV) console.error('Swap failed:', error); toast({ title: t('common.error'), description: error instanceof Error ? error.message : t('swap.swapFailed'), variant: 'destructive', }); setIsSwapping(false); } }; // Show DEX unavailable message if (!isDexAvailable && isAssetHubReady) { return (

{t('tokenSwap.dexComingSoon')}

{t('tokenSwap.dexComingDesc')}

{t('tokenSwap.scheduledUpgrade')}
); } return (
{/* Kurdistan Sun Animation Overlay during swap (only after confirm dialog is closed) */} {isSwapping && !showConfirm && (

{t('tokenSwap.processingSwap')}

)}
{/* Price Chart */} {exchangeRate > 0 && ( )}

{t('tokenSwap.tokenSwap')}

{!selectedAccount && ( {t('tokenSwap.connectWalletAlert')} )}
{t('tokenSwap.from')} {t('common.balance')} {fromBalance} {getTokenDisplayName(fromToken)}
setFromAmount(e.target.value)} placeholder="Amount" className="text-lg sm:text-2xl font-bold border-0 bg-transparent text-white placeholder:text-gray-500 placeholder:opacity-50" disabled={!selectedAccount} />
{t('tokenSwap.to')} {t('common.balance')} {toBalance} {getTokenDisplayName(toToken)}
{/* Swap Details - Uniswap Style */}
{t('common.exchangeRate')} {isLoadingRate ? ( t('common.loading') ) : exchangeRate > 0 ? ( `1 ${getTokenDisplayName(fromToken)} = ${exchangeRate.toFixed(4)} ${getTokenDisplayName(toToken)}` ) : ( t('tokenSwap.noPoolAvailable') )}
{/* Price Impact Indicator (Uniswap style) */} {fromAmount && parseFloat(fromAmount) > 0 && priceImpact > 0 && (
{t('tokenSwap.priceImpact')} {priceImpact < 0.01 ? '<0.01%' : `${priceImpact.toFixed(2)}%`}
)} {/* LP Fee */} {fromAmount && parseFloat(fromAmount) > 0 && lpFee && (
{t('tokenSwap.lpFee')} {lpFee} {getTokenDisplayName(fromToken)}
)} {/* Minimum Received */} {fromAmount && parseFloat(fromAmount) > 0 && minimumReceived && (
{t('tokenSwap.minimumReceived')} {minimumReceived} {getTokenDisplayName(toToken)}
)}
{t('common.slippageTolerance')} {slippage}%
{/* Insufficient Balance Warning */} {hasInsufficientBalance && ( {t('tokenSwap.insufficientWarning', { token: getTokenDisplayName(fromToken), balance: fromBalance, amount: fromAmount })} )} {/* High Price Impact Warning (>5%) */} {priceImpact >= 5 && !hasInsufficientBalance && ( {t('tokenSwap.highPriceImpact')} )}

{t('tokenSwap.liquidityPools')}

{isLoadingPools ? (
{t('tokenSwap.loadingPools')}
) : liquidityPools.length > 0 ? (
{liquidityPools.map((pool, idx) => (
{pool.pool}
TVL: {pool.tvl}
{pool.apr} APR
Vol: {pool.volume}
))}
) : (
{t('tokenSwap.noPoolsAvailable')}
)}

{t('tokenSwap.recentSwaps')}

{!selectedAccount ? (
{t('tokenSwap.connectForHistory')}
) : isLoadingHistory ? (
{t('tokenSwap.loadingHistory')}
) : swapHistory.length > 0 ? (
{swapHistory.map((tx, idx) => (
{getTokenDisplayName(tx.fromToken)} → {getTokenDisplayName(tx.toToken)}
#{tx.blockNumber}
{t('tokenSwap.sent')} -{tx.fromAmount} {getTokenDisplayName(tx.fromToken)}
{t('tokenSwap.received')} +{tx.toAmount} {getTokenDisplayName(tx.toToken)}
{new Date(tx.timestamp).toLocaleDateString()} {new Date(tx.timestamp).toLocaleTimeString()}
))}
) : (
{t('tokenSwap.noSwapHistory')}
)}
{t('tokenSwap.swapSettings')}
{['0.1', '0.5', '1.0'].map(val => ( ))} setSlippage(e.target.value)} className="w-20" />
{t('tokenSwap.confirmSwap')}
{t('tokenSwap.youPay')} {fromAmount} {getTokenDisplayName(fromToken)}
{t('tokenSwap.youReceive')} {toAmount} {getTokenDisplayName(toToken)}
{t('common.exchangeRate')} 1 {getTokenDisplayName(fromToken)} = {exchangeRate.toFixed(4)} {getTokenDisplayName(toToken)}
{t('swap.slippage')} {slippage}%
); }; export default TokenSwap;