import React, { useState, useEffect } from 'react'; import { usePezkuwi } from '@/contexts/PezkuwiContext'; import { useWallet } from '@/contexts/WalletContext'; import { X, Minus, AlertCircle, Loader2, CheckCircle, Info } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { PoolInfo, NATIVE_TOKEN_ID } from '@/types/dex'; import { formatTokenBalance } from '@pezkuwi/utils/dex'; // Helper to convert asset ID to XCM Location format for assetConversion pallet const formatAssetLocation = (id: number) => { if (id === NATIVE_TOKEN_ID) { // Native token from relay chain return { parents: 1, interior: 'Here' }; } // Asset on Asset Hub - XCM location format with PalletInstance 50 (assets pallet) return { parents: 0, interior: { X2: [{ PalletInstance: 50 }, { GeneralIndex: id }] } }; }; interface RemoveLiquidityModalProps { isOpen: boolean; pool: PoolInfo | null; onClose: () => void; onSuccess?: () => void; } type TransactionStatus = 'idle' | 'signing' | 'submitting' | 'success' | 'error'; export const RemoveLiquidityModal: React.FC = ({ isOpen, pool, onClose, onSuccess, }) => { // Use Asset Hub API for DEX operations (assetConversion pallet is on Asset Hub) const { assetHubApi, isAssetHubReady } = usePezkuwi(); const { account, signer } = useWallet(); const [lpTokenBalance, setLpTokenBalance] = useState('0'); const [removePercentage, setRemovePercentage] = useState(25); const [slippage, setSlippage] = useState(1); // 1% default const [txStatus, setTxStatus] = useState('idle'); const [errorMessage, setErrorMessage] = useState(''); // Reset form when modal closes or pool changes useEffect(() => { if (!isOpen || !pool) { setRemovePercentage(25); setTxStatus('idle'); setErrorMessage(''); } }, [isOpen, pool]); // Fetch LP token balance useEffect(() => { const fetchLPBalance = async () => { if (!assetHubApi || !isAssetHubReady || !account || !pool) return; try { // Get pool account using XCM Location format const asset1Location = formatAssetLocation(pool.asset1); const asset2Location = formatAssetLocation(pool.asset2); const poolAccount = await assetHubApi.query.assetConversion.pools([ asset1Location, asset2Location, ]); if (poolAccount.isNone) { setLpTokenBalance('0'); return; } // LP token ID is derived from pool ID // For now, we'll query the pool's LP token supply // In a real implementation, you'd need to query the specific LP token for the user if (assetHubApi.query.assetConversion.nextPoolAssetId) { await assetHubApi.query.assetConversion.nextPoolAssetId(); } // This is a simplified version - you'd need to track LP tokens properly setLpTokenBalance('0'); // Placeholder } catch (error) { if (import.meta.env.DEV) console.error('Failed to fetch LP balance:', error); setLpTokenBalance('0'); } }; fetchLPBalance(); }, [assetHubApi, isAssetHubReady, account, pool]); const calculateOutputAmounts = () => { if (!pool || BigInt(lpTokenBalance) === BigInt(0)) { return { amount1: '0', amount2: '0' }; } // Calculate amounts based on percentage const lpAmount = (BigInt(lpTokenBalance) * BigInt(removePercentage)) / BigInt(100); // Simplified calculation - in reality, this depends on total LP supply const totalLiquidity = BigInt(pool.reserve1) + BigInt(pool.reserve2); const userShare = lpAmount; // Proportional amounts const amount1 = (BigInt(pool.reserve1) * userShare) / totalLiquidity; const amount2 = (BigInt(pool.reserve2) * userShare) / totalLiquidity; return { amount1: amount1.toString(), amount2: amount2.toString(), }; }; const handleRemoveLiquidity = async () => { if (!assetHubApi || !isAssetHubReady || !signer || !account || !pool) { setErrorMessage('Wallet not connected'); return; } if (BigInt(lpTokenBalance) === BigInt(0)) { setErrorMessage('No liquidity to remove'); return; } const lpAmount = (BigInt(lpTokenBalance) * BigInt(removePercentage)) / BigInt(100); const { amount1, amount2 } = calculateOutputAmounts(); // Calculate minimum amounts with slippage tolerance // Formula: minAmount = amount * (100 - slippage%) / 100 // For 1% slippage: minAmount = amount * 99 / 100 const slippageBasisPoints = Math.floor(slippage * 100); // Convert percentage to basis points const minAmount1 = (BigInt(amount1) * BigInt(10000 - slippageBasisPoints)) / BigInt(10000); const minAmount2 = (BigInt(amount2) * BigInt(10000 - slippageBasisPoints)) / BigInt(10000); try { setTxStatus('signing'); setErrorMessage(''); // Use XCM Location format for assets (required for native token support) const asset1Location = formatAssetLocation(pool.asset1); const asset2Location = formatAssetLocation(pool.asset2); const tx = assetHubApi.tx.assetConversion.removeLiquidity( asset1Location, asset2Location, lpAmount.toString(), minAmount1.toString(), minAmount2.toString(), account ); setTxStatus('submitting'); await tx.signAndSend( account, { signer }, ({ status, dispatchError }) => { if (status.isInBlock) { if (dispatchError) { if (dispatchError.isModule) { const decoded = assetHubApi.registry.findMetaError(dispatchError.asModule); setErrorMessage(`${decoded.section}.${decoded.name}: ${decoded.docs}`); } else { setErrorMessage(dispatchError.toString()); } setTxStatus('error'); } else { setTxStatus('success'); setTimeout(() => { onSuccess?.(); onClose(); }, 2000); } } } ); } catch (error) { if (import.meta.env.DEV) console.error('Remove liquidity failed:', error); setErrorMessage(error instanceof Error ? error.message : 'Transaction failed'); setTxStatus('error'); } }; if (!isOpen || !pool) return null; const { amount1, amount2 } = calculateOutputAmounts(); return (
Remove Liquidity
{pool.asset1Symbol} / {pool.asset2Symbol} Pool
{/* Info Banner */}
Remove liquidity to receive your tokens back. You'll burn LP tokens in proportion to your withdrawal.
{/* LP Token Balance */}
Your LP Tokens
{formatTokenBalance(lpTokenBalance, 12, 6)}
{/* Percentage Selector */}
{removePercentage}%
setRemovePercentage(Number(e.target.value))} className="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-green-500" disabled={txStatus === 'signing' || txStatus === 'submitting'} />
{[25, 50, 75, 100].map((value) => ( ))}
{/* Divider */}
{/* Output Preview */}
You will receive
{pool.asset1Symbol} {formatTokenBalance(amount1, pool.asset1Decimals, 6)}
{pool.asset2Symbol} {formatTokenBalance(amount2, pool.asset2Decimals, 6)}
{/* Slippage Tolerance */}
{[0.5, 1, 2].map((value) => ( ))}
{/* Error Message */} {errorMessage && (
{errorMessage}
)} {/* Success Message */} {txStatus === 'success' && (
Liquidity removed successfully!
)} {/* Action Buttons */}
); };