diff --git a/src/components/TokenSwap.tsx b/src/components/TokenSwap.tsx index 8cf3df31..0f9038de 100644 --- a/src/components/TokenSwap.tsx +++ b/src/components/TokenSwap.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { ArrowDownUp, Settings, TrendingUp, Clock, AlertCircle } from 'lucide-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'; @@ -43,12 +43,28 @@ const TokenSwap = () => { 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 using AMM constant product formula - const toAmount = React.useMemo(() => { - if (!fromAmount || !poolReserves || parseFloat(fromAmount) <= 0) return ''; + // 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; @@ -70,6 +86,16 @@ const TokenSwap = () => { 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, @@ -78,11 +104,21 @@ const TokenSwap = () => { numerator, denominator, amountOut, + priceImpact: priceImpact.toFixed(2) + '%', + lpFeeAmount, + minReceived, feePercent: LP_FEE / 10 + '%' }); - return amountOut.toFixed(4); - }, [fromAmount, poolReserves, fromToken]); + 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(() => { @@ -296,6 +332,82 @@ const TokenSwap = () => { 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)) { + const [who, path, amountIn, amountOut] = event.data; + + // Parse path to get token symbols + const fromAssetId = path[0]?.nativeOrAsset?.asset?.toNumber() || 0; + const toAssetId = path[1]?.nativeOrAsset?.asset?.toNumber() || 0; + + 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); @@ -451,9 +563,63 @@ const TokenSwap = () => { setFromAmount(''); - // Refresh balances without page reload + // 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)) { + const [who, path, amountIn, amountOut] = event.data; + const fromAssetId = path[0]?.nativeOrAsset?.asset?.toNumber() || 0; + const toAssetId = path[1]?.nativeOrAsset?.asset?.toNumber() || 0; + 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', @@ -600,10 +766,14 @@ const TokenSwap = () => { -
+ {/* Swap Details - Uniswap Style */} +
- Exchange Rate - + + + Exchange Rate + + {isLoadingRate ? ( 'Loading...' ) : exchangeRate > 0 ? ( @@ -613,12 +783,60 @@ const TokenSwap = () => { )}
-
- Slippage Tolerance + + {/* 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. + + + )} +
diff --git a/src/components/p2p/P2PMarket.tsx b/src/components/p2p/P2PMarket.tsx index aab750dc..6e810267 100644 --- a/src/components/p2p/P2PMarket.tsx +++ b/src/components/p2p/P2PMarket.tsx @@ -6,7 +6,7 @@ import { Label } from '@/components/ui/label'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Badge } from '@/components/ui/badge'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { ArrowUpDown, Search, Filter, TrendingUp, TrendingDown, User, Shield, Clock, DollarSign } from 'lucide-react'; +import { ArrowUpDown, Search, Filter, TrendingUp, TrendingDown, User, Shield, Clock, DollarSign, Plus, X, SlidersHorizontal } from 'lucide-react'; import { useTranslation } from 'react-i18next'; interface P2POffer { @@ -35,6 +35,19 @@ export const P2PMarket: React.FC = () => { const [selectedOffer, setSelectedOffer] = useState(null); const [tradeAmount, setTradeAmount] = useState(''); + // Advanced filters + const [paymentMethodFilter, setPaymentMethodFilter] = useState('all'); + const [minPrice, setMinPrice] = useState(''); + const [maxPrice, setMaxPrice] = useState(''); + const [sortBy, setSortBy] = useState<'price' | 'rating' | 'trades'>('price'); + const [showFilters, setShowFilters] = useState(false); + + // Order creation + const [showCreateOrder, setShowCreateOrder] = useState(false); + const [newOrderAmount, setNewOrderAmount] = useState(''); + const [newOrderPrice, setNewOrderPrice] = useState(''); + const [newOrderPaymentMethod, setNewOrderPaymentMethod] = useState('Bank Transfer'); + const offers: P2POffer[] = [ { id: '1', @@ -106,11 +119,37 @@ export const P2PMarket: React.FC = () => { } ]; - const filteredOffers = offers.filter(offer => - offer.type === activeTab && - offer.token === selectedToken && - (searchTerm === '' || offer.seller.name.toLowerCase().includes(searchTerm.toLowerCase())) - ); + // Payment methods list + const paymentMethods = ['Bank Transfer', 'PayPal', 'Crypto', 'Wire Transfer', 'Cash', 'Mobile Money']; + + // Advanced filtering and sorting + const filteredOffers = offers + .filter(offer => { + // Basic filters + if (offer.type !== activeTab) return false; + if (offer.token !== selectedToken) return false; + if (searchTerm && !offer.seller.name.toLowerCase().includes(searchTerm.toLowerCase())) return false; + + // Payment method filter + if (paymentMethodFilter !== 'all' && offer.paymentMethod !== paymentMethodFilter) return false; + + // Price range filter + if (minPrice && offer.price < parseFloat(minPrice)) return false; + if (maxPrice && offer.price > parseFloat(maxPrice)) return false; + + return true; + }) + .sort((a, b) => { + // Sorting logic + if (sortBy === 'price') { + return activeTab === 'buy' ? a.price - b.price : b.price - a.price; + } else if (sortBy === 'rating') { + return b.seller.rating - a.seller.rating; + } else if (sortBy === 'trades') { + return b.seller.completedTrades - a.seller.completedTrades; + } + return 0; + }); const handleTrade = (offer: P2POffer) => { console.log('Initiating trade:', tradeAmount, offer.token, 'with', offer.seller.name); @@ -178,7 +217,27 @@ export const P2PMarket: React.FC = () => {
- {/* Filters */} + {/* Top Action Bar */} +
+ + + +
+ + {/* Basic Filters */}
setActiveTab(v as 'buy' | 'sell')} className="flex-1"> @@ -208,8 +267,90 @@ export const P2PMarket: React.FC = () => { />
+ + {/* Sort Selector */} +
+ {/* Advanced Filters Panel (Binance P2P style) */} + {showFilters && ( + +
+

+ + Advanced Filters +

+ +
+ {/* Payment Method Filter */} +
+ + +
+ + {/* Min Price Filter */} +
+ + setMinPrice(e.target.value)} + className="bg-gray-900 border-gray-700" + /> +
+ + {/* Max Price Filter */} +
+ + setMaxPrice(e.target.value)} + className="bg-gray-900 border-gray-700" + /> +
+
+ + {/* Clear Filters Button */} + +
+
+ )} + {/* Offers List */}
{filteredOffers.map((offer) => ( @@ -325,6 +466,130 @@ export const P2PMarket: React.FC = () => { )} + + {/* Create Order Modal (Binance P2P style) */} + {showCreateOrder && ( + + +
+ Create P2P Order + +
+ + Create a {activeTab === 'buy' ? 'buy' : 'sell'} order for {selectedToken} + +
+ +
+ + setActiveTab(v as 'buy' | 'sell')}> + + Buy + Sell + + +
+ +
+ + +
+ +
+ + setNewOrderAmount(e.target.value)} + className="bg-gray-800 border-gray-700" + /> +
+ +
+ + setNewOrderPrice(e.target.value)} + className="bg-gray-800 border-gray-700" + /> +
+ +
+ + +
+ +
+
+ Total Value + + ${(parseFloat(newOrderAmount || '0') * parseFloat(newOrderPrice || '0')).toFixed(2)} + +
+
+ +
+ + +
+ +
+ Note: Blockchain integration for P2P orders is coming soon +
+
+
+ )} + + {/* Overlay */} + {(showCreateOrder || selectedOffer) && ( +
{ + setShowCreateOrder(false); + setSelectedOffer(null); + }}>
+ )}
); }; \ No newline at end of file