diff --git a/src/components/TokenSwap.tsx b/src/components/TokenSwap.tsx index 0f9038de..37c12129 100644 --- a/src/components/TokenSwap.tsx +++ b/src/components/TokenSwap.tsx @@ -11,6 +11,8 @@ 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(); @@ -693,6 +695,15 @@ const TokenSwap = () => { )}
+ {/* Price Chart */} + {exchangeRate > 0 && ( + + )} +

Token Swap

@@ -876,6 +887,13 @@ const TokenSwap = () => {
)}
+ + {/* Limit Orders Section */} +
diff --git a/src/components/p2p/P2PMarket.tsx b/src/components/p2p/P2PMarket.tsx index 6e810267..97130203 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, Plus, X, SlidersHorizontal } from 'lucide-react'; +import { ArrowUpDown, Search, Filter, TrendingUp, TrendingDown, User, Shield, Clock, DollarSign, Plus, X, SlidersHorizontal, Lock, CheckCircle, AlertCircle } from 'lucide-react'; import { useTranslation } from 'react-i18next'; interface P2POffer { @@ -151,9 +151,34 @@ export const P2PMarket: React.FC = () => { return 0; }); + // Escrow state + const [showEscrow, setShowEscrow] = useState(false); + const [escrowStep, setEscrowStep] = useState<'funding' | 'confirmation' | 'release'>('funding'); + const [escrowOffer, setEscrowOffer] = useState(null); + const handleTrade = (offer: P2POffer) => { console.log('Initiating trade:', tradeAmount, offer.token, 'with', offer.seller.name); - // Implement trade logic + setEscrowOffer(offer); + setShowEscrow(true); + setEscrowStep('funding'); + }; + + const handleEscrowFund = () => { + console.log('Funding escrow with:', tradeAmount, escrowOffer?.token); + setEscrowStep('confirmation'); + }; + + const handleEscrowConfirm = () => { + console.log('Confirming payment received'); + setEscrowStep('release'); + }; + + const handleEscrowRelease = () => { + console.log('Releasing escrow funds'); + setShowEscrow(false); + setSelectedOffer(null); + setEscrowOffer(null); + setEscrowStep('funding'); }; return ( @@ -583,11 +608,189 @@ export const P2PMarket: React.FC = () => { )} + {/* Escrow Modal (Binance P2P Escrow style) */} + {showEscrow && escrowOffer && ( + + +
+ + + Secure Escrow Trade + + +
+ + Trade safely with escrow protection • {activeTab === 'buy' ? 'Buying' : 'Selling'} {escrowOffer.token} + +
+ + {/* Escrow Steps Indicator */} +
+ {[ + { step: 'funding', label: 'Fund Escrow', icon: Lock }, + { step: 'confirmation', label: 'Payment', icon: Clock }, + { step: 'release', label: 'Complete', icon: CheckCircle } + ].map((item, idx) => ( +
+
idx ? 'bg-green-600' : 'bg-gray-700' + }`}> + +
+ {item.label} + {idx < 2 && ( +
idx ? 'bg-green-600' : 'bg-gray-700' + }`} style={{ left: `calc(${(idx + 1) * 33.33}% - 64px)` }}>
+ )} +
+ ))} +
+ + {/* Trade Details Card */} + +

Trade Details

+
+
+ Seller + {escrowOffer.seller.name} +
+
+ Amount + {tradeAmount} {escrowOffer.token} +
+
+ Price per {escrowOffer.token} + ${escrowOffer.price} +
+
+ Payment Method + {escrowOffer.paymentMethod} +
+
+ Total + + ${(parseFloat(tradeAmount || '0') * escrowOffer.price).toFixed(2)} + +
+
+
+ + {/* Step Content */} + {escrowStep === 'funding' && ( +
+
+
+ +
+ Escrow Protection: Your funds will be held securely in smart contract escrow until both parties confirm the trade. This protects both buyer and seller. +
+
+
+ +
+ 1. Fund the escrow with {tradeAmount} {escrowOffer.token}
+ 2. Wait for seller to provide payment details
+ 3. Complete payment via {escrowOffer.paymentMethod}
+ 4. Confirm payment to release escrow +
+ + +
+ )} + + {escrowStep === 'confirmation' && ( +
+
+
+ +
+ Waiting for Payment: Complete your {escrowOffer.paymentMethod} payment and click confirm when done. Do not release escrow until payment is verified! +
+
+
+ + +

Payment Instructions

+
+

• Payment Method: {escrowOffer.paymentMethod}

+

• Amount: ${(parseFloat(tradeAmount || '0') * escrowOffer.price).toFixed(2)}

+

• Time Limit: {escrowOffer.timeLimit} minutes

+
+
+ +
+ + +
+
+ )} + + {escrowStep === 'release' && ( +
+
+
+ +
+ Payment Confirmed: Your payment has been verified. The escrow will be released to the seller automatically. +
+
+
+ + +

Trade Summary

+
+

✅ Escrow Funded: {tradeAmount} {escrowOffer.token}

+

✅ Payment Sent: ${(parseFloat(tradeAmount || '0') * escrowOffer.price).toFixed(2)}

+

✅ Payment Verified

+

🎉 Trade Completed Successfully!

+
+
+ + +
+ )} + +
+ Note: Smart contract escrow integration coming soon +
+
+
+ )} + {/* Overlay */} - {(showCreateOrder || selectedOffer) && ( + {(showCreateOrder || selectedOffer || showEscrow) && (
{ setShowCreateOrder(false); setSelectedOffer(null); + setShowEscrow(false); }}>
)}
diff --git a/src/components/trading/LimitOrders.tsx b/src/components/trading/LimitOrders.tsx new file mode 100644 index 00000000..4b7aaa10 --- /dev/null +++ b/src/components/trading/LimitOrders.tsx @@ -0,0 +1,306 @@ +import React, { useState } from 'react'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Badge } from '@/components/ui/badge'; +import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { X, Clock, CheckCircle, AlertCircle } from 'lucide-react'; + +interface LimitOrder { + id: string; + type: 'buy' | 'sell'; + fromToken: string; + toToken: string; + fromAmount: number; + limitPrice: number; + currentPrice: number; + status: 'pending' | 'filled' | 'cancelled' | 'expired'; + createdAt: number; + expiresAt: number; +} + +interface LimitOrdersProps { + fromToken: string; + toToken: string; + currentPrice: number; + onCreateOrder?: (order: Omit) => void; +} + +export const LimitOrders: React.FC = ({ + fromToken, + toToken, + currentPrice, + onCreateOrder +}) => { + const [orderType, setOrderType] = useState<'buy' | 'sell'>('buy'); + const [amount, setAmount] = useState(''); + const [limitPrice, setLimitPrice] = useState(''); + const [showCreateForm, setShowCreateForm] = useState(false); + + // Mock orders (in production, fetch from blockchain) + const [orders, setOrders] = useState([ + { + id: '1', + type: 'buy', + fromToken: 'PEZ', + toToken: 'HEZ', + fromAmount: 100, + limitPrice: 0.98, + currentPrice: 1.02, + status: 'pending', + createdAt: Date.now() - 3600000, + expiresAt: Date.now() + 82800000 + }, + { + id: '2', + type: 'sell', + fromToken: 'HEZ', + toToken: 'PEZ', + fromAmount: 50, + limitPrice: 1.05, + currentPrice: 1.02, + status: 'pending', + createdAt: Date.now() - 7200000, + expiresAt: Date.now() + 79200000 + } + ]); + + const handleCreateOrder = () => { + const newOrder: Omit = { + type: orderType, + fromToken: orderType === 'buy' ? toToken : fromToken, + toToken: orderType === 'buy' ? fromToken : toToken, + fromAmount: parseFloat(amount), + limitPrice: parseFloat(limitPrice), + currentPrice + }; + + console.log('Creating limit order:', newOrder); + + // Add to orders list (mock) + const order: LimitOrder = { + ...newOrder, + id: Date.now().toString(), + status: 'pending', + createdAt: Date.now(), + expiresAt: Date.now() + 86400000 // 24 hours + }; + + setOrders([order, ...orders]); + setShowCreateForm(false); + setAmount(''); + setLimitPrice(''); + + if (onCreateOrder) { + onCreateOrder(newOrder); + } + }; + + const handleCancelOrder = (orderId: string) => { + setOrders(orders.map(order => + order.id === orderId ? { ...order, status: 'cancelled' as const } : order + )); + }; + + const getStatusBadge = (status: LimitOrder['status']) => { + switch (status) { + case 'pending': + return + + Pending + ; + case 'filled': + return + + Filled + ; + case 'cancelled': + return + + Cancelled + ; + case 'expired': + return + + Expired + ; + } + }; + + const getPriceDistance = (order: LimitOrder) => { + const distance = ((order.limitPrice - order.currentPrice) / order.currentPrice) * 100; + return distance; + }; + + return ( + + +
+
+ Limit Orders + + Set orders to execute at your target price + +
+ +
+
+ + {showCreateForm && ( + +
+
+ + setOrderType(v as 'buy' | 'sell')}> + + Buy {fromToken} + Sell {fromToken} + + +
+ +
+ + setAmount(e.target.value)} + className="bg-gray-900 border-gray-700" + /> +
+ +
+ + setLimitPrice(e.target.value)} + className="bg-gray-900 border-gray-700" + /> +
+ Current market price: ${currentPrice.toFixed(4)} +
+
+ +
+
+ You will {orderType} + + {amount || '0'} {orderType === 'buy' ? fromToken : toToken} + +
+
+ When price reaches + + ${limitPrice || '0'} per {fromToken} + +
+
+ Estimated total + + {((parseFloat(amount || '0') * parseFloat(limitPrice || '0'))).toFixed(2)} {orderType === 'buy' ? toToken : fromToken} + +
+
+ + + +
+ Order will expire in 24 hours if not filled +
+
+
+ )} + + {/* Orders List */} +
+ {orders.length === 0 ? ( +
+ No limit orders yet. Create one to get started! +
+ ) : ( + orders.map(order => { + const priceDistance = getPriceDistance(order); + return ( + +
+
+ + {order.type.toUpperCase()} + + + {order.fromToken} → {order.toToken} + +
+ {getStatusBadge(order.status)} +
+ +
+
+
Amount
+
+ {order.fromAmount} {order.fromToken} +
+
+
+
Limit Price
+
+ ${order.limitPrice.toFixed(4)} +
+
+
+
Current Price
+
+ ${order.currentPrice.toFixed(4)} +
+
+
+
Distance
+
0 ? 'text-green-400' : 'text-red-400'}> + {priceDistance > 0 ? '+' : ''}{priceDistance.toFixed(2)}% +
+
+
+ +
+ + Created {new Date(order.createdAt).toLocaleString()} + + {order.status === 'pending' && ( + + )} +
+
+ ); + }) + )} +
+ +
+ Note: Limit orders require blockchain integration to execute automatically +
+
+
+ ); +}; diff --git a/src/components/trading/PriceChart.tsx b/src/components/trading/PriceChart.tsx new file mode 100644 index 00000000..75f5af01 --- /dev/null +++ b/src/components/trading/PriceChart.tsx @@ -0,0 +1,160 @@ +import React, { useState, useEffect } from 'react'; +import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, Area, AreaChart } from 'recharts'; +import { Card } from '@/components/ui/card'; +import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { TrendingUp, TrendingDown } from 'lucide-react'; + +interface PriceChartProps { + fromToken: string; + toToken: string; + currentPrice: number; +} + +export const PriceChart: React.FC = ({ fromToken, toToken, currentPrice }) => { + const [timeframe, setTimeframe] = useState<'1H' | '24H' | '7D' | '30D'>('24H'); + const [chartData, setChartData] = useState([]); + const [priceChange, setPriceChange] = useState<{ value: number; percent: number }>({ value: 0, percent: 0 }); + + useEffect(() => { + // Generate mock historical data (in production, fetch from blockchain/oracle) + const generateMockData = () => { + const dataPoints = timeframe === '1H' ? 60 : timeframe === '24H' ? 24 : timeframe === '7D' ? 7 : 30; + const basePrice = currentPrice || 1.0; + + const data = []; + let price = basePrice * 0.95; // Start 5% below current + + for (let i = 0; i < dataPoints; i++) { + // Random walk with slight upward trend + const change = (Math.random() - 0.48) * 0.02; // Slight bullish bias + price = price * (1 + change); + + let timeLabel = ''; + const now = new Date(); + + if (timeframe === '1H') { + now.setMinutes(now.getMinutes() - (dataPoints - i)); + timeLabel = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); + } else if (timeframe === '24H') { + now.setHours(now.getHours() - (dataPoints - i)); + timeLabel = now.toLocaleTimeString('en-US', { hour: '2-digit' }); + } else if (timeframe === '7D') { + now.setDate(now.getDate() - (dataPoints - i)); + timeLabel = now.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); + } else { + now.setDate(now.getDate() - (dataPoints - i)); + timeLabel = now.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); + } + + data.push({ + time: timeLabel, + price: parseFloat(price.toFixed(4)), + timestamp: now.getTime() + }); + } + + // Add current price as last point + data.push({ + time: 'Now', + price: basePrice, + timestamp: Date.now() + }); + + return data; + }; + + const data = generateMockData(); + setChartData(data); + + // Calculate price change + if (data.length > 1) { + const firstPrice = data[0].price; + const lastPrice = data[data.length - 1].price; + const change = lastPrice - firstPrice; + const changePercent = (change / firstPrice) * 100; + setPriceChange({ value: change, percent: changePercent }); + } + }, [timeframe, currentPrice]); + + const isPositive = priceChange.percent >= 0; + + return ( + +
+
+
+ {fromToken}/{toToken} Price +
+
+ + ${currentPrice.toFixed(4)} + +
+ {isPositive ? : } + {isPositive ? '+' : ''}{priceChange.percent.toFixed(2)}% +
+
+
+ + setTimeframe(v as any)}> + + 1H + 24H + 7D + 30D + + +
+ + + + + + + + + + + `$${value.toFixed(3)}`} + /> + [`$${value.toFixed(4)}`, 'Price']} + /> + + + + +
+ Historical price data • Updated in real-time +
+
+ ); +};