mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-30 17:17:56 +00:00
feat: Add comprehensive liquidity pool management system
- Add AddLiquidityModal with automatic HEZ to wHEZ wrapping and 10% slippage tolerance - Add RemoveLiquidityModal with automatic wHEZ to HEZ unwrapping - Add PoolDashboard component with metrics, APR calculation, and impermanent loss calculator - Add /pool route and integrate Pool button in WalletDashboard - Display real-time pool reserves, TVL, and user positions - Support batched transactions for optimal UX 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import PasswordReset from '@/pages/PasswordReset';
|
||||
import ProfileSettings from '@/pages/ProfileSettings';
|
||||
import AdminPanel from '@/pages/AdminPanel';
|
||||
import WalletDashboard from './pages/WalletDashboard';
|
||||
import PoolDashboardPage from './pages/PoolDashboard';
|
||||
import { AppProvider } from '@/contexts/AppContext';
|
||||
import { PolkadotProvider } from '@/contexts/PolkadotContext';
|
||||
import { WalletProvider } from '@/contexts/WalletContext';
|
||||
@@ -56,6 +57,11 @@ function App() {
|
||||
<WalletDashboard />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/pool" element={
|
||||
<ProtectedRoute>
|
||||
<PoolDashboardPage />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { X, Plus, Info, AlertCircle } from 'lucide-react';
|
||||
import { web3FromAddress } from '@polkadot/extension-dapp';
|
||||
import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||
import { useWallet } from '@/contexts/WalletContext';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
|
||||
interface AddLiquidityModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({ isOpen, onClose }) => {
|
||||
const { api, selectedAccount, isApiReady } = usePolkadot();
|
||||
const { balances, refreshBalances } = useWallet();
|
||||
|
||||
const [whezAmount, setWhezAmount] = useState('');
|
||||
const [pezAmount, setPezAmount] = useState('');
|
||||
const [currentPrice, setCurrentPrice] = useState<number | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
// Fetch current pool price
|
||||
useEffect(() => {
|
||||
if (!api || !isApiReady || !isOpen) return;
|
||||
|
||||
const fetchPoolPrice = async () => {
|
||||
try {
|
||||
const poolId = [0, 1];
|
||||
const poolInfo = await api.query.assetConversion.pools(poolId);
|
||||
|
||||
if (poolInfo.isSome) {
|
||||
const lpTokenData = poolInfo.unwrap().toJSON() as any;
|
||||
const lpTokenId = lpTokenData.lpToken;
|
||||
|
||||
// Get pool account
|
||||
const poolAccountData = await api.query.assetConversion.poolAccountIds?.(poolId);
|
||||
let poolAccount = '';
|
||||
|
||||
if (poolAccountData && poolAccountData.isSome) {
|
||||
poolAccount = poolAccountData.unwrap().toString();
|
||||
|
||||
// Get reserves
|
||||
const whezBalanceData = await api.query.assets.account(0, poolAccount);
|
||||
const pezBalanceData = await api.query.assets.account(1, poolAccount);
|
||||
|
||||
if (whezBalanceData.isSome && pezBalanceData.isSome) {
|
||||
const whezData = whezBalanceData.unwrap().toJSON() as any;
|
||||
const pezData = pezBalanceData.unwrap().toJSON() as any;
|
||||
|
||||
const reserve0 = Number(whezData.balance) / 1e12;
|
||||
const reserve1 = Number(pezData.balance) / 1e12;
|
||||
|
||||
setCurrentPrice(reserve1 / reserve0);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching pool price:', err);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPoolPrice();
|
||||
}, [api, isApiReady, isOpen]);
|
||||
|
||||
// Auto-calculate PEZ amount based on wHEZ input
|
||||
useEffect(() => {
|
||||
if (whezAmount && currentPrice) {
|
||||
const calculatedPez = parseFloat(whezAmount) * currentPrice;
|
||||
setPezAmount(calculatedPez.toFixed(4));
|
||||
} else if (!whezAmount) {
|
||||
setPezAmount('');
|
||||
}
|
||||
}, [whezAmount, currentPrice]);
|
||||
|
||||
const handleAddLiquidity = async () => {
|
||||
if (!api || !selectedAccount || !whezAmount || !pezAmount) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Validate amounts
|
||||
if (parseFloat(whezAmount) <= 0 || parseFloat(pezAmount) <= 0) {
|
||||
setError('Please enter valid amounts');
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (parseFloat(whezAmount) > whezBalance) {
|
||||
setError('Insufficient HEZ balance');
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (parseFloat(pezAmount) > pezBalance) {
|
||||
setError('Insufficient PEZ balance');
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the signer from the extension
|
||||
const injector = await web3FromAddress(selectedAccount.address);
|
||||
|
||||
const whezAmountBN = BigInt(Math.floor(parseFloat(whezAmount) * 1e12));
|
||||
const pezAmountBN = BigInt(Math.floor(parseFloat(pezAmount) * 1e12));
|
||||
|
||||
// Min amounts (90% of desired to account for slippage - more tolerance for AMM)
|
||||
const minWhezBN = (whezAmountBN * BigInt(90)) / BigInt(100);
|
||||
const minPezBN = (pezAmountBN * BigInt(90)) / BigInt(100);
|
||||
|
||||
// Need to wrap HEZ to wHEZ first
|
||||
const wrapTx = api.tx.tokenWrapper.wrap(whezAmountBN.toString());
|
||||
|
||||
// Add liquidity transaction
|
||||
const addLiquidityTx = api.tx.assetConversion.addLiquidity(
|
||||
0, // asset1 (wHEZ)
|
||||
1, // asset2 (PEZ)
|
||||
whezAmountBN.toString(),
|
||||
pezAmountBN.toString(),
|
||||
minWhezBN.toString(),
|
||||
minPezBN.toString(),
|
||||
selectedAccount.address
|
||||
);
|
||||
|
||||
// Batch transactions
|
||||
const tx = api.tx.utility.batchAll([wrapTx, addLiquidityTx]);
|
||||
|
||||
await tx.signAndSend(
|
||||
selectedAccount.address,
|
||||
{ signer: injector.signer },
|
||||
({ status, events, dispatchError }) => {
|
||||
if (status.isInBlock) {
|
||||
console.log('Transaction in block:', status.asInBlock.toHex());
|
||||
} else if (status.isFinalized) {
|
||||
console.log('Transaction finalized:', status.asFinalized.toHex());
|
||||
|
||||
// Check for errors
|
||||
const hasError = events.some(({ event }) =>
|
||||
api.events.system.ExtrinsicFailed.is(event)
|
||||
);
|
||||
|
||||
if (hasError || dispatchError) {
|
||||
let errorMessage = 'Transaction failed';
|
||||
|
||||
if (dispatchError) {
|
||||
if (dispatchError.isModule) {
|
||||
const decoded = api.registry.findMetaError(dispatchError.asModule);
|
||||
const { docs, name, section } = decoded;
|
||||
errorMessage = `${section}.${name}: ${docs.join(' ')}`;
|
||||
console.error('Dispatch error:', errorMessage);
|
||||
} else {
|
||||
errorMessage = dispatchError.toString();
|
||||
console.error('Dispatch error:', errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// Also check events for more details
|
||||
events.forEach(({ event }) => {
|
||||
if (api.events.system.ExtrinsicFailed.is(event)) {
|
||||
console.error('ExtrinsicFailed event:', event.toHuman());
|
||||
}
|
||||
});
|
||||
|
||||
setError(errorMessage);
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
console.log('Transaction successful');
|
||||
setSuccess(true);
|
||||
setIsLoading(false);
|
||||
setWhezAmount('');
|
||||
setPezAmount('');
|
||||
refreshBalances();
|
||||
|
||||
setTimeout(() => {
|
||||
setSuccess(false);
|
||||
onClose();
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('Error adding liquidity:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to add liquidity');
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const whezBalance = balances.HEZ || 0;
|
||||
const pezBalance = balances.PEZ || 0;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-gray-900 rounded-lg max-w-md w-full p-6 border border-gray-700">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-2xl font-bold text-white">Add Liquidity</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-white transition-colors"
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<Alert className="mb-4 bg-red-900/20 border-red-500">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<Alert className="mb-4 bg-green-900/20 border-green-500">
|
||||
<AlertDescription>Liquidity added successfully!</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Alert className="mb-4 bg-blue-900/20 border-blue-500">
|
||||
<Info className="h-4 w-4" />
|
||||
<AlertDescription className="text-sm">
|
||||
Add liquidity to earn 3% fees from all swaps. Your HEZ will be automatically wrapped to wHEZ.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* HEZ Input */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
HEZ Amount
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="number"
|
||||
value={whezAmount}
|
||||
onChange={(e) => setWhezAmount(e.target.value)}
|
||||
placeholder="0.0"
|
||||
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-blue-500"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<div className="absolute right-3 top-3 flex items-center gap-2">
|
||||
<span className="text-gray-400 text-sm">HEZ</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between mt-1 text-xs text-gray-400">
|
||||
<span>Balance: {whezBalance.toLocaleString()}</span>
|
||||
<button
|
||||
onClick={() => setWhezAmount(whezBalance.toString())}
|
||||
className="text-blue-400 hover:text-blue-300"
|
||||
>
|
||||
Max
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Plus className="w-5 h-5 text-gray-400" />
|
||||
</div>
|
||||
|
||||
{/* PEZ Input */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
PEZ Amount (Auto-calculated)
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="number"
|
||||
value={pezAmount}
|
||||
placeholder="0.0"
|
||||
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-gray-400 focus:outline-none cursor-not-allowed"
|
||||
disabled={true}
|
||||
readOnly
|
||||
/>
|
||||
<div className="absolute right-3 top-3 flex items-center gap-2">
|
||||
<span className="text-gray-400 text-sm">PEZ</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between mt-1 text-xs text-gray-400">
|
||||
<span>Balance: {pezBalance.toLocaleString()}</span>
|
||||
<span>
|
||||
{currentPrice && `Rate: 1 HEZ = ${currentPrice.toFixed(4)} PEZ`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Price Info */}
|
||||
{whezAmount && pezAmount && (
|
||||
<div className="bg-gray-800 rounded-lg p-3 space-y-2 text-sm">
|
||||
<div className="flex justify-between text-gray-300">
|
||||
<span>Share of Pool</span>
|
||||
<span>~0.1%</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-gray-300">
|
||||
<span>Slippage Tolerance</span>
|
||||
<span>10%</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={handleAddLiquidity}
|
||||
disabled={
|
||||
isLoading ||
|
||||
!whezAmount ||
|
||||
!pezAmount ||
|
||||
parseFloat(whezAmount) > whezBalance ||
|
||||
parseFloat(pezAmount) > pezBalance
|
||||
}
|
||||
className="w-full bg-gradient-to-r from-green-600 to-blue-600 hover:from-green-700 hover:to-blue-700 h-12"
|
||||
>
|
||||
{isLoading ? 'Adding Liquidity...' : 'Add Liquidity'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,486 @@
|
||||
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 { usePolkadot } from '@/contexts/PolkadotContext';
|
||||
import { useWallet } from '@/contexts/WalletContext';
|
||||
import { ASSET_IDS } from '@/lib/wallet';
|
||||
import { AddLiquidityModal } from '@/components/AddLiquidityModal';
|
||||
import { RemoveLiquidityModal } from '@/components/RemoveLiquidityModal';
|
||||
|
||||
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<PoolData | null>(null);
|
||||
const [lpPosition, setLPPosition] = useState<LPPosition | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isAddLiquidityModalOpen, setIsAddLiquidityModalOpen] = useState(false);
|
||||
const [isRemoveLiquidityModalOpen, setIsRemoveLiquidityModalOpen] = useState(false);
|
||||
|
||||
// Fetch pool data
|
||||
useEffect(() => {
|
||||
if (!api || !isApiReady) return;
|
||||
|
||||
const fetchPoolData = async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Query wHEZ/PEZ pool
|
||||
const poolId = [0, 1]; // wHEZ (asset 0) / PEZ (asset 1)
|
||||
|
||||
const poolInfo = await api.query.assetConversion.pools(poolId);
|
||||
|
||||
if (poolInfo.isSome) {
|
||||
const lpTokenData = poolInfo.unwrap().toJSON() as any;
|
||||
const lpTokenId = lpTokenData.lpToken;
|
||||
|
||||
// Get pool account
|
||||
const poolAccountData = await api.query.assetConversion.poolAccountIds?.(poolId);
|
||||
let poolAccount = '';
|
||||
|
||||
if (poolAccountData && poolAccountData.isSome) {
|
||||
poolAccount = poolAccountData.unwrap().toString();
|
||||
}
|
||||
|
||||
// Get reserves
|
||||
const whezBalanceData = await api.query.assets.account(0, poolAccount);
|
||||
const pezBalanceData = await api.query.assets.account(1, poolAccount);
|
||||
|
||||
let reserve0 = 0;
|
||||
let reserve1 = 0;
|
||||
|
||||
if (whezBalanceData.isSome) {
|
||||
const whezData = whezBalanceData.unwrap().toJSON() as any;
|
||||
reserve0 = Number(whezData.balance) / 1e12;
|
||||
}
|
||||
|
||||
if (pezBalanceData.isSome) {
|
||||
const pezData = pezBalanceData.unwrap().toJSON() as any;
|
||||
reserve1 = Number(pezData.balance) / 1e12;
|
||||
}
|
||||
|
||||
setPoolData({
|
||||
asset0: 0,
|
||||
asset1: 1,
|
||||
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]);
|
||||
|
||||
// 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 (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4"></div>
|
||||
<p className="text-gray-400">Loading pool data...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert className="bg-red-900/20 border-red-500">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (!poolData) {
|
||||
return (
|
||||
<Alert className="bg-yellow-900/20 border-yellow-500">
|
||||
<Info className="h-4 w-4" />
|
||||
<AlertDescription>No pool data available</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white flex items-center gap-2">
|
||||
<Droplet className="h-6 w-6 text-blue-400" />
|
||||
wHEZ/PEZ Pool Dashboard
|
||||
</h2>
|
||||
<p className="text-gray-400 mt-1">Monitor liquidity pool metrics and your position</p>
|
||||
</div>
|
||||
<Badge variant="outline" className="flex items-center gap-1">
|
||||
<Clock className="h-3 w-3" />
|
||||
Live
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Key Metrics Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* Total Liquidity */}
|
||||
<Card className="p-4 bg-gray-800/50 border-gray-700">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">Total Liquidity</p>
|
||||
<p className="text-2xl font-bold text-white mt-1">
|
||||
${totalLiquidityUSD.toLocaleString('en-US', { maximumFractionDigits: 0 })}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
{poolData.reserve0.toLocaleString()} wHEZ + {poolData.reserve1.toLocaleString()} PEZ
|
||||
</p>
|
||||
</div>
|
||||
<DollarSign className="h-8 w-8 text-green-400" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Current Price */}
|
||||
<Card className="p-4 bg-gray-800/50 border-gray-700">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">HEZ Price</p>
|
||||
<p className="text-2xl font-bold text-white mt-1">
|
||||
{currentPrice.toFixed(4)} PEZ
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
1 PEZ = {(1 / currentPrice).toFixed(6)} HEZ
|
||||
</p>
|
||||
</div>
|
||||
<TrendingUp className="h-8 w-8 text-blue-400" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* APR */}
|
||||
<Card className="p-4 bg-gray-800/50 border-gray-700">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">Estimated APR</p>
|
||||
<p className="text-2xl font-bold text-white mt-1">
|
||||
{estimateAPR().toFixed(2)}%
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
From swap fees
|
||||
</p>
|
||||
</div>
|
||||
<Percent className="h-8 w-8 text-yellow-400" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Constant Product */}
|
||||
<Card className="p-4 bg-gray-800/50 border-gray-700">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">Constant (k)</p>
|
||||
<p className="text-2xl font-bold text-white mt-1">
|
||||
{(constantProduct / 1e9).toFixed(1)}B
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
x × y = k
|
||||
</p>
|
||||
</div>
|
||||
<BarChart3 className="h-8 w-8 text-purple-400" />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="reserves" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3 bg-gray-800">
|
||||
<TabsTrigger value="reserves">Reserves</TabsTrigger>
|
||||
<TabsTrigger value="position">Your Position</TabsTrigger>
|
||||
<TabsTrigger value="calculator">IL Calculator</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Reserves Tab */}
|
||||
<TabsContent value="reserves" className="space-y-4">
|
||||
<Card className="p-6 bg-gray-800/50 border-gray-700">
|
||||
<h3 className="text-lg font-semibold text-white mb-4">Pool Reserves</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-4 bg-gray-900/50 rounded-lg">
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">wHEZ Reserve</p>
|
||||
<p className="text-2xl font-bold text-white">{poolData.reserve0.toLocaleString('en-US', { maximumFractionDigits: 2 })}</p>
|
||||
</div>
|
||||
<Badge variant="outline">Asset 0</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 bg-gray-900/50 rounded-lg">
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">PEZ Reserve</p>
|
||||
<p className="text-2xl font-bold text-white">{poolData.reserve1.toLocaleString('en-US', { maximumFractionDigits: 2 })}</p>
|
||||
</div>
|
||||
<Badge variant="outline">Asset 1</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 p-4 bg-blue-900/20 border border-blue-500/30 rounded-lg">
|
||||
<div className="flex items-start gap-2">
|
||||
<Info className="h-4 w-4 text-blue-400 mt-0.5" />
|
||||
<div className="text-sm text-gray-300">
|
||||
<p className="font-semibold text-blue-400 mb-1">AMM Formula</p>
|
||||
<p>Pool maintains constant product: x × y = k</p>
|
||||
<p className="mt-2 font-mono text-xs">
|
||||
{poolData.reserve0.toFixed(2)} × {poolData.reserve1.toFixed(2)} = {constantProduct.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* Your Position Tab */}
|
||||
<TabsContent value="position" className="space-y-4">
|
||||
<Card className="p-6 bg-gray-800/50 border-gray-700">
|
||||
<h3 className="text-lg font-semibold text-white mb-4">Your Liquidity Position</h3>
|
||||
|
||||
{!selectedAccount ? (
|
||||
<Alert className="bg-yellow-900/20 border-yellow-500">
|
||||
<Info className="h-4 w-4" />
|
||||
<AlertDescription>Connect wallet to view your position</AlertDescription>
|
||||
</Alert>
|
||||
) : !lpPosition ? (
|
||||
<div className="text-center py-8 text-gray-400">
|
||||
<Droplet className="h-12 w-12 mx-auto mb-2 opacity-50" />
|
||||
<p>No liquidity position found</p>
|
||||
<Button
|
||||
onClick={() => setIsAddLiquidityModalOpen(true)}
|
||||
className="mt-4 bg-gradient-to-r from-green-600 to-blue-600 hover:from-green-700 hover:to-blue-700"
|
||||
>
|
||||
Add Liquidity
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="p-4 bg-gray-900/50 rounded-lg">
|
||||
<p className="text-sm text-gray-400">LP Tokens</p>
|
||||
<p className="text-xl font-bold text-white">{lpPosition.lpTokenBalance.toFixed(4)}</p>
|
||||
</div>
|
||||
<div className="p-4 bg-gray-900/50 rounded-lg">
|
||||
<p className="text-sm text-gray-400">Pool Share</p>
|
||||
<p className="text-xl font-bold text-white">{lpPosition.share.toFixed(4)}%</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-gray-900/50 rounded-lg">
|
||||
<p className="text-sm text-gray-400 mb-2">Your Position Value</p>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-300">wHEZ:</span>
|
||||
<span className="text-white font-semibold">{lpPosition.asset0Amount.toFixed(4)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-300">PEZ:</span>
|
||||
<span className="text-white font-semibold">{lpPosition.asset1Amount.toFixed(4)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-green-900/20 border border-green-500/30 rounded-lg">
|
||||
<p className="text-sm text-gray-400 mb-2">Estimated Earnings (APR {estimateAPR().toFixed(2)}%)</p>
|
||||
<div className="space-y-1 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-300">Daily:</span>
|
||||
<span className="text-green-400">~{((lpPosition.asset0Amount * 2 * estimateAPR()) / 365 / 100).toFixed(4)} HEZ</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-300">Monthly:</span>
|
||||
<span className="text-green-400">~{((lpPosition.asset0Amount * 2 * estimateAPR()) / 12 / 100).toFixed(4)} HEZ</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-300">Yearly:</span>
|
||||
<span className="text-green-400">~{((lpPosition.asset0Amount * 2 * estimateAPR()) / 100).toFixed(4)} HEZ</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 pt-2">
|
||||
<Button
|
||||
onClick={() => setIsAddLiquidityModalOpen(true)}
|
||||
className="bg-gradient-to-r from-green-600 to-blue-600 hover:from-green-700 hover:to-blue-700"
|
||||
>
|
||||
Add More
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setIsRemoveLiquidityModalOpen(true)}
|
||||
variant="outline"
|
||||
className="border-red-600 text-red-400 hover:bg-red-900/20"
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* Impermanent Loss Calculator Tab */}
|
||||
<TabsContent value="calculator" className="space-y-4">
|
||||
<Card className="p-6 bg-gray-800/50 border-gray-700">
|
||||
<h3 className="text-lg font-semibold text-white mb-4">Impermanent Loss Calculator</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-gray-900/50 rounded-lg">
|
||||
<p className="text-sm text-gray-400 mb-3">If HEZ price changes by:</p>
|
||||
|
||||
<div className="space-y-2">
|
||||
{[10, 25, 50, 100, 200].map((change) => {
|
||||
const il = calculateImpermanentLoss(change);
|
||||
return (
|
||||
<div key={change} className="flex justify-between items-center py-2 border-b border-gray-700 last:border-0">
|
||||
<span className="text-gray-300">+{change}%</span>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={il < -1 ? 'border-red-500 text-red-400' : 'border-yellow-500 text-yellow-400'}
|
||||
>
|
||||
{il.toFixed(2)}% Loss
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Alert className="bg-orange-900/20 border-orange-500">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<p className="font-semibold mb-1">What is Impermanent Loss?</p>
|
||||
<p className="text-sm text-gray-300">
|
||||
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.
|
||||
</p>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{/* Modals */}
|
||||
<AddLiquidityModal
|
||||
isOpen={isAddLiquidityModalOpen}
|
||||
onClose={() => setIsAddLiquidityModalOpen(false)}
|
||||
/>
|
||||
|
||||
{lpPosition && poolData && (
|
||||
<RemoveLiquidityModal
|
||||
isOpen={isRemoveLiquidityModalOpen}
|
||||
onClose={() => setIsRemoveLiquidityModalOpen(false)}
|
||||
lpPosition={lpPosition}
|
||||
lpTokenId={poolData.lpTokenId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PoolDashboard;
|
||||
@@ -0,0 +1,237 @@
|
||||
import React, { useState } from 'react';
|
||||
import { X, Minus, AlertCircle, Info } from 'lucide-react';
|
||||
import { web3FromAddress } from '@polkadot/extension-dapp';
|
||||
import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||
import { useWallet } from '@/contexts/WalletContext';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
|
||||
interface RemoveLiquidityModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
lpPosition: {
|
||||
lpTokenBalance: number;
|
||||
share: number;
|
||||
asset0Amount: number;
|
||||
asset1Amount: number;
|
||||
};
|
||||
lpTokenId: number;
|
||||
}
|
||||
|
||||
export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
lpPosition,
|
||||
lpTokenId,
|
||||
}) => {
|
||||
const { api, selectedAccount } = usePolkadot();
|
||||
const { refreshBalances } = useWallet();
|
||||
|
||||
const [percentage, setPercentage] = useState(100);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
const handleRemoveLiquidity = async () => {
|
||||
if (!api || !selectedAccount) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Get the signer from the extension
|
||||
const injector = await web3FromAddress(selectedAccount.address);
|
||||
|
||||
// Calculate LP tokens to remove
|
||||
const lpToRemove = (lpPosition.lpTokenBalance * percentage) / 100;
|
||||
const lpToRemoveBN = BigInt(Math.floor(lpToRemove * 1e12));
|
||||
|
||||
// Calculate expected token amounts (with 95% slippage tolerance)
|
||||
const expectedWhezBN = BigInt(Math.floor((lpPosition.asset0Amount * percentage) / 100 * 1e12));
|
||||
const expectedPezBN = BigInt(Math.floor((lpPosition.asset1Amount * percentage) / 100 * 1e12));
|
||||
|
||||
const minWhezBN = (expectedWhezBN * BigInt(95)) / BigInt(100);
|
||||
const minPezBN = (expectedPezBN * BigInt(95)) / BigInt(100);
|
||||
|
||||
// Remove liquidity transaction
|
||||
const removeLiquidityTx = api.tx.assetConversion.removeLiquidity(
|
||||
0, // asset1 (wHEZ)
|
||||
1, // asset2 (PEZ)
|
||||
lpToRemoveBN.toString(),
|
||||
minWhezBN.toString(),
|
||||
minPezBN.toString(),
|
||||
selectedAccount.address
|
||||
);
|
||||
|
||||
// Unwrap wHEZ back to HEZ
|
||||
const unwrapTx = api.tx.tokenWrapper.unwrap(minWhezBN.toString());
|
||||
|
||||
// Batch transactions
|
||||
const tx = api.tx.utility.batchAll([removeLiquidityTx, unwrapTx]);
|
||||
|
||||
await tx.signAndSend(
|
||||
selectedAccount.address,
|
||||
{ signer: injector.signer },
|
||||
({ status, events }) => {
|
||||
if (status.isInBlock) {
|
||||
console.log('Transaction in block');
|
||||
} else if (status.isFinalized) {
|
||||
console.log('Transaction finalized');
|
||||
|
||||
// Check for errors
|
||||
const hasError = events.some(({ event }) =>
|
||||
api.events.system.ExtrinsicFailed.is(event)
|
||||
);
|
||||
|
||||
if (hasError) {
|
||||
setError('Transaction failed');
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
setSuccess(true);
|
||||
setIsLoading(false);
|
||||
refreshBalances();
|
||||
|
||||
setTimeout(() => {
|
||||
setSuccess(false);
|
||||
onClose();
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('Error removing liquidity:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to remove liquidity');
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const whezToReceive = (lpPosition.asset0Amount * percentage) / 100;
|
||||
const pezToReceive = (lpPosition.asset1Amount * percentage) / 100;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-gray-900 rounded-lg max-w-md w-full p-6 border border-gray-700">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-2xl font-bold text-white">Remove Liquidity</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-white transition-colors"
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<Alert className="mb-4 bg-red-900/20 border-red-500">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<Alert className="mb-4 bg-green-900/20 border-green-500">
|
||||
<AlertDescription>Liquidity removed successfully!</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Alert className="mb-4 bg-blue-900/20 border-blue-500">
|
||||
<Info className="h-4 w-4" />
|
||||
<AlertDescription className="text-sm">
|
||||
Remove your liquidity to receive back your tokens. wHEZ will be automatically unwrapped to HEZ.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Percentage Selector */}
|
||||
<div>
|
||||
<div className="flex justify-between mb-2">
|
||||
<label className="text-sm font-medium text-gray-300">Amount to Remove</label>
|
||||
<span className="text-2xl font-bold text-white">{percentage}%</span>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="range"
|
||||
min="1"
|
||||
max="100"
|
||||
value={percentage}
|
||||
onChange={(e) => setPercentage(parseInt(e.target.value))}
|
||||
className="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-blue-500"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
|
||||
<div className="flex justify-between mt-2">
|
||||
{[25, 50, 75, 100].map((p) => (
|
||||
<button
|
||||
key={p}
|
||||
onClick={() => setPercentage(p)}
|
||||
className={`px-3 py-1 rounded text-sm ${
|
||||
percentage === p
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
||||
}`}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{p}%
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* You Will Receive */}
|
||||
<div className="bg-gray-800 rounded-lg p-4 space-y-3">
|
||||
<h3 className="text-sm font-medium text-gray-300 mb-2">You Will Receive</h3>
|
||||
|
||||
<div className="flex justify-between items-center p-3 bg-gray-900/50 rounded-lg">
|
||||
<div>
|
||||
<p className="text-xs text-gray-400">HEZ</p>
|
||||
<p className="text-xl font-bold text-white">
|
||||
{whezToReceive.toFixed(4)}
|
||||
</p>
|
||||
</div>
|
||||
<Minus className="w-5 h-5 text-gray-400" />
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center p-3 bg-gray-900/50 rounded-lg">
|
||||
<div>
|
||||
<p className="text-xs text-gray-400">PEZ</p>
|
||||
<p className="text-xl font-bold text-white">
|
||||
{pezToReceive.toFixed(4)}
|
||||
</p>
|
||||
</div>
|
||||
<Minus className="w-5 h-5 text-gray-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* LP Token Info */}
|
||||
<div className="bg-gray-800 rounded-lg p-3 space-y-2 text-sm">
|
||||
<div className="flex justify-between text-gray-300">
|
||||
<span>LP Tokens to Burn</span>
|
||||
<span>{((lpPosition.lpTokenBalance * percentage) / 100).toFixed(4)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-gray-300">
|
||||
<span>Remaining LP Tokens</span>
|
||||
<span>
|
||||
{((lpPosition.lpTokenBalance * (100 - percentage)) / 100).toFixed(4)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-gray-300">
|
||||
<span>Slippage Tolerance</span>
|
||||
<span>5%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleRemoveLiquidity}
|
||||
disabled={isLoading || percentage === 0}
|
||||
className="w-full bg-gradient-to-r from-red-600 to-orange-600 hover:from-red-700 hover:to-orange-700 h-12"
|
||||
>
|
||||
{isLoading ? 'Removing Liquidity...' : 'Remove Liquidity'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import PoolDashboard from '@/components/PoolDashboard';
|
||||
|
||||
const PoolDashboardPage = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-950 pt-24 pb-12">
|
||||
<div className="container mx-auto px-4 py-8 relative">
|
||||
<button
|
||||
onClick={() => navigate('/wallet')}
|
||||
className="absolute top-4 left-4 text-red-500 hover:text-red-400 transition-colors flex items-center gap-2 font-semibold"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
<span>Back to Wallet</span>
|
||||
</button>
|
||||
<PoolDashboard />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PoolDashboardPage;
|
||||
@@ -6,7 +6,7 @@ import { TransferModal } from '@/components/TransferModal';
|
||||
import { ReceiveModal } from '@/components/ReceiveModal';
|
||||
import { TransactionHistory } from '@/components/TransactionHistory';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ArrowUpRight, ArrowDownRight, History, ArrowLeft } from 'lucide-react';
|
||||
import { ArrowUpRight, ArrowDownRight, History, ArrowLeft, Activity } from 'lucide-react';
|
||||
|
||||
const WalletDashboard: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
@@ -49,7 +49,7 @@ const WalletDashboard: React.FC = () => {
|
||||
{/* Right Column - Actions */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* Quick Actions */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
||||
<Button
|
||||
onClick={() => setIsTransferModalOpen(true)}
|
||||
className="bg-gradient-to-r from-green-600 to-yellow-400 hover:from-green-700 hover:to-yellow-500 h-24 flex flex-col items-center justify-center"
|
||||
@@ -57,7 +57,7 @@ const WalletDashboard: React.FC = () => {
|
||||
<ArrowUpRight className="w-6 h-6 mb-2" />
|
||||
<span>Send</span>
|
||||
</Button>
|
||||
|
||||
|
||||
<Button
|
||||
onClick={() => setIsReceiveModalOpen(true)}
|
||||
variant="outline"
|
||||
@@ -67,6 +67,15 @@ const WalletDashboard: React.FC = () => {
|
||||
<span>Receive</span>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => navigate('/pool')}
|
||||
variant="outline"
|
||||
className="border-blue-600 hover:bg-blue-900/20 text-blue-400 h-24 flex flex-col items-center justify-center"
|
||||
>
|
||||
<Activity className="w-6 h-6 mb-2" />
|
||||
<span>Pool</span>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => setIsHistoryModalOpen(true)}
|
||||
variant="outline"
|
||||
|
||||
Reference in New Issue
Block a user