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 { usePolkadot } from '@/contexts/PolkadotContext'; import { useWallet } from '@/contexts/WalletContext'; import { ASSET_IDS, formatBalance, parseAmount } from '@/lib/wallet'; import { useToast } from '@/hooks/use-toast'; import { KurdistanSun } from './KurdistanSun'; import { PriceChart } from './trading/PriceChart'; import { LimitOrders } from './trading/LimitOrders'; const TokenSwap = () => { const { api, isApiReady, selectedAccount } = usePolkadot(); const { balances, refreshBalances } = useWallet(); const { toast } = useToast(); 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 console.log('πŸ” TokenSwap balances from context:', balances); console.log('πŸ” fromToken:', fromToken, 'toToken:', toToken); const fromBalance = balances[fromToken as keyof typeof balances]; const toBalance = balances[toToken as keyof typeof balances]; console.log('πŸ” Final balances:', { fromBalance, toBalance }); // Liquidity pool data 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 } | null>(null); // 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, asset1 } = poolReserves; // Determine which reserve is input and which is output const fromAssetId = fromToken === 'HEZ' ? 0 : ASSET_IDS[fromToken as keyof typeof ASSET_IDS]; const isAsset0ToAsset1 = fromAssetId === asset0; const reserveIn = isAsset0ToAsset1 ? reserve0 : reserve1; const reserveOut = isAsset0ToAsset1 ? reserve1 : reserve0; // Uniswap V2 AMM formula (matches Substrate runtime exactly) // Runtime: amount_in_with_fee = amount_in * (1000 - LPFee) = amount_in * 970 // LPFee = 30 (3% fee, not 0.3%!) // Formula: amountOut = (amountIn * 970 * reserveOut) / (reserveIn * 1000 + amountIn * 970) const LP_FEE = 30; // 3% fee const amountInWithFee = amountIn * (1000 - LP_FEE); // = amountIn * 970 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 const minReceived = (amountOut * (1 - parseFloat(slippage) / 100)).toFixed(4); console.log('πŸ” Uniswap V2 AMM:', { amountIn, amountInWithFee, reserveIn, reserveOut, numerator, denominator, amountOut, priceImpact: priceImpact.toFixed(2) + '%', lpFeeAmount, minReceived, feePercent: LP_FEE / 10 + '%' }); return { toAmount: amountOut.toFixed(4), priceImpact, minimumReceived: minReceived, lpFee: lpFeeAmount }; }, [fromAmount, poolReserves, fromToken, slippage]); const { toAmount, priceImpact, minimumReceived, lpFee } = swapCalculations; // Check if AssetConversion pallet is available useEffect(() => { console.log('πŸ” Checking DEX availability...', { api: !!api, isApiReady }); if (api && isApiReady) { const hasAssetConversion = api.tx.assetConversion !== undefined; console.log('πŸ” AssetConversion pallet check:', hasAssetConversion); setIsDexAvailable(hasAssetConversion); if (!hasAssetConversion) { console.warn('⚠️ AssetConversion pallet not available in runtime'); } else { console.log('βœ… AssetConversion pallet is available!'); } } }, [api, isApiReady]); // Fetch exchange rate from AssetConversion pool // Always use wHEZ/PEZ pool (the only valid pool) useEffect(() => { const fetchExchangeRate = async () => { console.log('πŸ” fetchExchangeRate check:', { api: !!api, isApiReady, isDexAvailable, fromToken, toToken }); if (!api || !isApiReady || !isDexAvailable) { console.log('⚠️ Skipping fetchExchangeRate:', { api: !!api, isApiReady, isDexAvailable }); return; } console.log('βœ… Starting fetchExchangeRate...'); setIsLoadingRate(true); try { // Map user-selected tokens to actual pool assets // HEZ β†’ wHEZ (Asset 0) behind the scenes const getPoolAssetId = (token: string) => { if (token === 'HEZ') return 0; // wHEZ return ASSET_IDS[token as keyof typeof ASSET_IDS]; }; const fromAssetId = getPoolAssetId(fromToken); const toAssetId = getPoolAssetId(toToken); console.log('πŸ” Looking for pool:', { fromToken, toToken, fromAssetId, toAssetId }); // IMPORTANT: Pool ID must be sorted (smaller asset ID first) const [asset1, asset2] = fromAssetId < toAssetId ? [fromAssetId, toAssetId] : [toAssetId, fromAssetId]; console.log('πŸ” Sorted pool assets:', { asset1, asset2 }); // Create pool asset tuple [asset1, asset2] - must be sorted! const poolAssets = [ { NativeOrAsset: { Asset: asset1 } }, { NativeOrAsset: { Asset: asset2 } } ]; console.log('πŸ” Pool query with:', poolAssets); // Query pool from AssetConversion pallet const poolInfo = await api.query.assetConversion.pools(poolAssets); console.log('πŸ” Pool query result:', poolInfo.toHuman()); console.log('πŸ” Pool isEmpty?', poolInfo.isEmpty, 'exists?', !poolInfo.isEmpty); if (poolInfo && !poolInfo.isEmpty) { const pool = poolInfo.toJSON() as any; console.log('πŸ” Pool data:', pool); try { // New pallet version: reserves are stored in pool account balances // AccountIdConverter implementation in substrate: // blake2_256(&Encode::encode(&(PalletId, PoolId))[..]) console.log('πŸ” Deriving 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 poolId = api.createType('(u32, u32)', [asset1, asset2]); console.log('πŸ” Pool ID:', poolId.toHuman()); // 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, poolId]); console.log('πŸ” Full tuple encoded length:', fullTuple.toU8a().length); console.log('πŸ” Full tuple bytes:', Array.from(fullTuple.toU8a())); // Hash the SCALE-encoded tuple const accountHash = blake2AsU8a(fullTuple.toU8a(), 256); console.log('πŸ” Account hash:', Array.from(accountHash).slice(0, 8)); const poolAccountId = api.createType('AccountId32', accountHash); console.log('πŸ” Pool AccountId (NEW METHOD):', poolAccountId.toString()); // Query pool account's asset balances console.log('πŸ” Querying reserves for asset', asset1, 'and', asset2); const reserve0Query = await api.query.assets.account(asset1, poolAccountId); const reserve1Query = await api.query.assets.account(asset2, poolAccountId); console.log('πŸ” Reserve0 query result:', reserve0Query.toHuman()); console.log('πŸ” Reserve1 query result:', reserve1Query.toHuman()); console.log('πŸ” Reserve0 isEmpty?', reserve0Query.isEmpty); console.log('πŸ” Reserve1 isEmpty?', reserve1Query.isEmpty); const reserve0Data = reserve0Query.toJSON() as any; const reserve1Data = reserve1Query.toJSON() as any; console.log('πŸ” Reserve0 JSON:', reserve0Data); console.log('πŸ” Reserve1 JSON:', reserve1Data); if (reserve0Data && reserve1Data && reserve0Data.balance && reserve1Data.balance) { // Parse hex string balances to BigInt, then to number const balance0Hex = reserve0Data.balance.toString(); const balance1Hex = reserve1Data.balance.toString(); console.log('πŸ” Raw hex balances:', { balance0Hex, balance1Hex }); const reserve0 = Number(BigInt(balance0Hex)) / 1e12; const reserve1 = Number(BigInt(balance1Hex)) / 1e12; console.log('βœ… Reserves found:', { reserve0, reserve1 }); // Store pool reserves for AMM calculation setPoolReserves({ reserve0, reserve1, asset0: asset1, // Sorted pool always has asset1 < asset2 asset1: asset2 }); // Also calculate simple exchange rate for display const rate = fromAssetId === asset1 ? reserve1 / reserve0 // from asset1 to asset2 : reserve0 / reserve1; // from asset2 to asset1 console.log('βœ… Exchange rate:', rate, 'direction:', fromAssetId === asset1 ? 'asset1β†’asset2' : 'asset2β†’asset1'); setExchangeRate(rate); } else { console.warn('⚠️ Pool has no reserves - reserve0Data:', reserve0Data, 'reserve1Data:', reserve1Data); setExchangeRate(0); } } catch (err) { console.error('❌ Error deriving pool account:', err); setExchangeRate(0); } } else { console.warn('No liquidity pool found for this pair'); setExchangeRate(0); } } catch (error) { console.error('Failed to fetch exchange rate:', error); setExchangeRate(0); } finally { setIsLoadingRate(false); } }; fetchExchangeRate(); }, [api, isApiReady, isDexAvailable, fromToken, toToken]); // Fetch liquidity pools useEffect(() => { const fetchLiquidityPools = async () => { if (!api || !isApiReady || !isDexAvailable) { return; } setIsLoadingPools(true); try { // Query all pools from AssetConversion pallet const poolsEntries = await api.query.assetConversion.pools.entries(); if (poolsEntries && poolsEntries.length > 0) { const pools = poolsEntries.map(([key, value]: any) => { 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) { console.error('Failed to fetch liquidity pools:', error); setLiquidityPools([]); } finally { setIsLoadingPools(false); } }; fetchLiquidityPools(); }, [api, isApiReady, isDexAvailable]); // Fetch swap transaction history useEffect(() => { const fetchSwapHistory = async () => { if (!api || !isApiReady || !isDexAvailable || !selectedAccount) { return; } setIsLoadingHistory(true); try { // Get recent finalized blocks (last 100 blocks) const finalizedHead = await api.rpc.chain.getFinalizedHead(); const finalizedBlock = await api.rpc.chain.getBlock(finalizedHead); const currentBlockNumber = finalizedBlock.block.header.number.toNumber(); const startBlock = Math.max(0, currentBlockNumber - 100); 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 api.rpc.chain.getBlockHash(blockNum); const apiAt = await api.at(blockHash); const events = await apiAt.query.system.events(); const block = await api.rpc.chain.getBlock(blockHash); const timestamp = Date.now() - ((currentBlockNumber - blockNum) * 6000); // Estimate 6s per block events.forEach((record: any) => { const { event } = record; // Check for AssetConversion::SwapExecuted event if (api.events.assetConversion?.SwapExecuted?.is(event)) { console.log('πŸ” Full event.data:', event.data.toJSON()); console.log('πŸ” event.data length:', event.data.length); // SwapExecuted has 5 fields: (who, send_to, amountIn, amountOut, path) const [who, sendTo, amountIn, amountOut, path] = event.data; console.log('πŸ” who:', who.toString()); console.log('πŸ” sendTo:', sendTo.toString()); console.log('πŸ” path type:', typeof path); console.log('πŸ” path:', path); // Parse path to get token symbols - path is Vec let fromAssetId = 0; let toAssetId = 0; try { // Try different path formats const pathArray = path.toJSON ? path.toJSON() : path; console.log('πŸ” Raw path data:', JSON.stringify(pathArray, null, 2)); if (Array.isArray(pathArray) && pathArray.length >= 2) { // Extract asset IDs from path const asset0 = pathArray[0]; const asset1 = pathArray[1]; console.log('πŸ” Asset0 structure:', JSON.stringify(asset0, null, 2)); console.log('πŸ” Asset1 structure:', JSON.stringify(asset1, null, 2)); // Path structure is: [[assetId, amount], [assetId, amount]] // 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; } } console.log('πŸ” Parsed IDs:', { fromAssetId, toAssetId }); } catch (err) { console.warn('Failed to parse path:', err); } const fromTokenSymbol = fromAssetId === 0 ? 'wHEZ' : fromAssetId === 1 ? 'PEZ' : `Asset${fromAssetId}`; const toTokenSymbol = toAssetId === 0 ? 'wHEZ' : toAssetId === 1 ? 'PEZ' : `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) { console.warn(`Failed to fetch block ${blockNum}:`, err); } } console.log('βœ… Swap history fetched:', transactions.length, 'transactions'); setSwapHistory(transactions.slice(0, 10)); // Show max 10 } catch (error) { console.error('Failed to fetch swap history:', error); setSwapHistory([]); } finally { setIsLoadingHistory(false); } }; fetchSwapHistory(); }, [api, isApiReady, isDexAvailable, selectedAccount]); const handleSwap = () => { setFromToken(toToken); setToToken(fromToken); setFromAmount(''); }; const handleConfirmSwap = async () => { if (!api || !selectedAccount) { toast({ title: 'Error', description: 'Please connect your wallet', variant: 'destructive', }); return; } if (!isDexAvailable) { toast({ title: 'DEX Not Available', description: 'AssetConversion pallet is not enabled in runtime', variant: 'destructive', }); return; } if (!exchangeRate || exchangeRate === 0) { toast({ title: 'Error', description: 'No liquidity pool available for this pair', variant: 'destructive', }); return; } setIsSwapping(true); setShowConfirm(false); // Close dialog before transaction starts try { const amountIn = parseAmount(fromAmount, 12); const minAmountOut = parseAmount( (parseFloat(toAmount) * (1 - parseFloat(slippage) / 100)).toString(), 12 ); // Get signer from extension const { web3FromAddress } = await import('@polkadot/extension-dapp'); const injector = await web3FromAddress(selectedAccount.address); // Build transaction based on token types let tx; if (fromToken === 'HEZ' && toToken === 'PEZ') { // HEZ β†’ PEZ: wrap(HEZβ†’wHEZ) then swap(wHEZβ†’PEZ) const wrapTx = api.tx.tokenWrapper.wrap(amountIn.toString()); const swapPath = [ { NativeOrAsset: { Asset: 0 } }, // wHEZ { NativeOrAsset: { Asset: 1 } } // PEZ ]; const swapTx = api.tx.assetConversion.swapExactTokensForTokens( swapPath, amountIn.toString(), minAmountOut.toString(), selectedAccount.address, true ); tx = api.tx.utility.batchAll([wrapTx, swapTx]); } else if (fromToken === 'PEZ' && toToken === 'HEZ') { // PEZ β†’ HEZ: swap(PEZβ†’wHEZ) then unwrap(wHEZβ†’HEZ) const swapPath = [ { NativeOrAsset: { Asset: 1 } }, // PEZ { NativeOrAsset: { Asset: 0 } } // wHEZ ]; const swapTx = api.tx.assetConversion.swapExactTokensForTokens( swapPath, amountIn.toString(), minAmountOut.toString(), selectedAccount.address, true ); const unwrapTx = api.tx.tokenWrapper.unwrap(minAmountOut.toString()); tx = api.tx.utility.batchAll([swapTx, unwrapTx]); } else { // Direct swap between assets (should not happen with HEZ/PEZ only) const getPoolAssetId = (token: string) => { if (token === 'HEZ') return 0; // wHEZ return ASSET_IDS[token as keyof typeof ASSET_IDS]; }; const swapPath = [ { NativeOrAsset: { Asset: getPoolAssetId(fromToken) } }, { NativeOrAsset: { Asset: getPoolAssetId(toToken) } } ]; tx = api.tx.assetConversion.swapExactTokensForTokens( swapPath, amountIn.toString(), minAmountOut.toString(), selectedAccount.address, true ); } // Sign and send transaction await tx.signAndSend( selectedAccount.address, { signer: injector.signer }, async ({ status, events, dispatchError }) => { console.log('πŸ” Transaction status:', status.toHuman()); if (status.isInBlock) { console.log('βœ… Transaction in block:', status.asInBlock.toHex()); toast({ title: 'Transaction Submitted', description: `Processing in block ${status.asInBlock.toHex().slice(0, 10)}...`, }); } if (status.isFinalized) { console.log('βœ… Transaction finalized:', status.asFinalized.toHex()); console.log('πŸ” All events:', events.map(({ event }) => event.toHuman())); console.log('πŸ” dispatchError:', dispatchError?.toHuman()); // Check for errors if (dispatchError) { let errorMessage = 'Transaction failed'; if (dispatchError.isModule) { const decoded = api.registry.findMetaError(dispatchError.asModule); errorMessage = `${decoded.section}.${decoded.name}: ${decoded.docs}`; } toast({ title: 'Error', description: errorMessage, variant: 'destructive', }); setIsSwapping(false); return; } // Success - check for swap event const hasSwapEvent = events.some(({ event }) => api.events.assetConversion?.SwapExecuted?.is(event) ); if (hasSwapEvent || fromToken === 'HEZ' || toToken === 'HEZ') { toast({ title: 'Success!', description: `Swapped ${fromAmount} ${fromToken} for ~${toAmount} ${toToken}`, }); setFromAmount(''); // Refresh balances and history without page reload await refreshBalances(); console.log('βœ… Balances refreshed after swap'); // Refresh swap history after 3 seconds (wait for block finalization) setTimeout(async () => { console.log('πŸ”„ Refreshing swap history...'); const fetchSwapHistory = async () => { if (!api || !isApiReady || !isDexAvailable || !selectedAccount) return; setIsLoadingHistory(true); try { const finalizedHead = await api.rpc.chain.getFinalizedHead(); const finalizedBlock = await api.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 api.rpc.chain.getBlockHash(blockNum); const apiAt = await api.at(blockHash); const events = await apiAt.query.system.events(); const timestamp = Date.now() - ((currentBlockNumber - blockNum) * 6000); events.forEach((record: any) => { const { event } = record; if (api.events.assetConversion?.SwapExecuted?.is(event)) { console.log('πŸ”„ Full event.data:', event.data.toJSON()); console.log('πŸ”„ event.data length:', event.data.length); // SwapExecuted has 5 fields: (who, send_to, amountIn, amountOut, path) const [who, sendTo, amountIn, amountOut, path] = event.data; console.log('πŸ”„ who:', who.toString()); console.log('πŸ”„ sendTo:', sendTo.toString()); console.log('πŸ”„ path type:', typeof path); console.log('πŸ”„ path:', path); // Parse path (same logic as main history fetch) let fromAssetId = 0; let toAssetId = 0; try { const pathArray = path.toJSON ? path.toJSON() : path; console.log('πŸ”„ Refresh path data:', JSON.stringify(pathArray, null, 2)); if (Array.isArray(pathArray) && pathArray.length >= 2) { const asset0 = pathArray[0]; const asset1 = pathArray[1]; console.log('πŸ”„ Refresh Asset0:', JSON.stringify(asset0, null, 2)); console.log('πŸ”„ Refresh Asset1:', JSON.stringify(asset1, null, 2)); // Path structure is: [[assetId, amount], [assetId, amount]] // 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; } } console.log('πŸ”„ Refresh Parsed IDs:', { fromAssetId, toAssetId }); } catch (err) { console.warn('Failed to parse path in refresh:', err); } const fromTokenSymbol = fromAssetId === 0 ? 'wHEZ' : fromAssetId === 1 ? 'PEZ' : `Asset${fromAssetId}`; const toTokenSymbol = toAssetId === 0 ? 'wHEZ' : toAssetId === 1 ? 'PEZ' : `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) { console.warn(`Failed to fetch block ${blockNum}:`, err); } } setSwapHistory(transactions.slice(0, 10)); } catch (error) { console.error('Failed to refresh swap history:', error); } finally { setIsLoadingHistory(false); } }; await fetchSwapHistory(); }, 3000); } else { toast({ title: 'Error', description: 'Swap transaction failed', variant: 'destructive', }); } setIsSwapping(false); } } ); } catch (error: any) { console.error('Swap failed:', error); toast({ title: 'Error', description: error.message || 'Swap transaction failed', variant: 'destructive', }); setIsSwapping(false); } }; // Show DEX unavailable message if (!isDexAvailable && isApiReady) { return (

DEX Coming Soon

The AssetConversion pallet is not yet enabled in the runtime. Token swapping functionality will be available after the next runtime upgrade.

Scheduled for Next Runtime Upgrade
); } return (
{/* Kurdistan Sun Animation Overlay during swap (only after confirm dialog is closed) */} {isSwapping && !showConfirm && (

Processing your swap...

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

Token Swap

{!selectedAccount && ( Please connect your wallet to swap tokens )}
From Balance: {fromBalance} {fromToken}
setFromAmount(e.target.value)} placeholder="0.0" className="text-2xl font-bold border-0 bg-transparent text-white placeholder:text-gray-600" disabled={!selectedAccount} />
To Balance: {toBalance} {toToken}
{/* Swap Details - Uniswap Style */}
Exchange Rate {isLoadingRate ? ( 'Loading...' ) : exchangeRate > 0 ? ( `1 ${fromToken} = ${exchangeRate.toFixed(4)} ${toToken}` ) : ( 'No pool available' )}
{/* Price Impact Indicator (Uniswap style) */} {fromAmount && parseFloat(fromAmount) > 0 && priceImpact > 0 && (
Price Impact {priceImpact < 0.01 ? '<0.01%' : `${priceImpact.toFixed(2)}%`}
)} {/* LP Fee */} {fromAmount && parseFloat(fromAmount) > 0 && lpFee && (
Liquidity Provider Fee {lpFee} {fromToken}
)} {/* Minimum Received */} {fromAmount && parseFloat(fromAmount) > 0 && minimumReceived && (
Minimum Received {minimumReceived} {toToken}
)}
Slippage Tolerance {slippage}%
{/* High Price Impact Warning (>5%) */} {priceImpact >= 5 && ( High price impact! Your trade will significantly affect the pool price. Consider a smaller amount or check if there's better liquidity. )}

Liquidity Pools

{isLoadingPools ? (
Loading pools...
) : liquidityPools.length > 0 ? (
{liquidityPools.map((pool, idx) => (
{pool.pool}
TVL: {pool.tvl}
{pool.apr} APR
Vol: {pool.volume}
))}
) : (
No liquidity pools available yet
)}
{/* Limit Orders Section */}

Recent Swaps

{!selectedAccount ? (
Connect wallet to view history
) : isLoadingHistory ? (
Loading history...
) : swapHistory.length > 0 ? (
{swapHistory.map((tx, idx) => (
{tx.fromToken} β†’ {tx.toToken}
#{tx.blockNumber}
Sent: -{tx.fromAmount} {tx.fromToken}
Received: +{tx.toAmount} {tx.toToken}
{new Date(tx.timestamp).toLocaleDateString()} {new Date(tx.timestamp).toLocaleTimeString()}
))}
) : (
No swap history yet
)}
Swap Settings
{['0.1', '0.5', '1.0'].map(val => ( ))} setSlippage(e.target.value)} className="w-20" />
Confirm Swap
You Pay {fromAmount} {fromToken}
You Receive {toAmount} {toToken}
Exchange Rate 1 {fromToken} = {exchangeRate.toFixed(4)} {toToken}
Slippage {slippage}%
); }; export default TokenSwap;