mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-06-17 10:01:03 +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 ProfileSettings from '@/pages/ProfileSettings';
|
||||||
import AdminPanel from '@/pages/AdminPanel';
|
import AdminPanel from '@/pages/AdminPanel';
|
||||||
import WalletDashboard from './pages/WalletDashboard';
|
import WalletDashboard from './pages/WalletDashboard';
|
||||||
|
import PoolDashboardPage from './pages/PoolDashboard';
|
||||||
import { AppProvider } from '@/contexts/AppContext';
|
import { AppProvider } from '@/contexts/AppContext';
|
||||||
import { PolkadotProvider } from '@/contexts/PolkadotContext';
|
import { PolkadotProvider } from '@/contexts/PolkadotContext';
|
||||||
import { WalletProvider } from '@/contexts/WalletContext';
|
import { WalletProvider } from '@/contexts/WalletContext';
|
||||||
@@ -56,6 +57,11 @@ function App() {
|
|||||||
<WalletDashboard />
|
<WalletDashboard />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
|
<Route path="/pool" element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<PoolDashboardPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</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 { ReceiveModal } from '@/components/ReceiveModal';
|
||||||
import { TransactionHistory } from '@/components/TransactionHistory';
|
import { TransactionHistory } from '@/components/TransactionHistory';
|
||||||
import { Button } from '@/components/ui/button';
|
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 WalletDashboard: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -49,7 +49,7 @@ const WalletDashboard: React.FC = () => {
|
|||||||
{/* Right Column - Actions */}
|
{/* Right Column - Actions */}
|
||||||
<div className="lg:col-span-2 space-y-6">
|
<div className="lg:col-span-2 space-y-6">
|
||||||
{/* Quick Actions */}
|
{/* 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
|
<Button
|
||||||
onClick={() => setIsTransferModalOpen(true)}
|
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"
|
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" />
|
<ArrowUpRight className="w-6 h-6 mb-2" />
|
||||||
<span>Send</span>
|
<span>Send</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setIsReceiveModalOpen(true)}
|
onClick={() => setIsReceiveModalOpen(true)}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -67,6 +67,15 @@ const WalletDashboard: React.FC = () => {
|
|||||||
<span>Receive</span>
|
<span>Receive</span>
|
||||||
</Button>
|
</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
|
<Button
|
||||||
onClick={() => setIsHistoryModalOpen(true)}
|
onClick={() => setIsHistoryModalOpen(true)}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
Reference in New Issue
Block a user