import React, { useState, useEffect } from 'react'; import { usePolkadot } from '@/contexts/PolkadotContext'; import { useWallet } from '@/contexts/WalletContext'; import { X, Plus, AlertCircle, Loader2, CheckCircle, Info } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { PoolInfo } from '@/types/dex'; import { parseTokenInput, formatTokenBalance, quote } from '@pezkuwi/utils/dex'; interface AddLiquidityModalProps { isOpen: boolean; pool: PoolInfo | null; onClose: () => void; onSuccess?: () => void; } type TransactionStatus = 'idle' | 'signing' | 'submitting' | 'success' | 'error'; export const AddLiquidityModal: React.FC = ({ isOpen, pool, onClose, onSuccess, }) => { const { api, isApiReady } = usePolkadot(); const { account, signer } = useWallet(); const [amount1Input, setAmount1Input] = useState(''); const [amount2Input, setAmount2Input] = useState(''); const [slippage, setSlippage] = useState(1); // 1% default const [balance1, setBalance1] = useState('0'); const [balance2, setBalance2] = useState('0'); const [txStatus, setTxStatus] = useState('idle'); const [errorMessage, setErrorMessage] = useState(''); // Reset form when modal closes or pool changes useEffect(() => { if (!isOpen || !pool) { setAmount1Input(''); setAmount2Input(''); setTxStatus('idle'); setErrorMessage(''); } }, [isOpen, pool]); // Fetch balances useEffect(() => { const fetchBalances = async () => { if (!api || !isApiReady || !account || !pool) return; try { const balance1Data = await api.query.assets.account(pool.asset1, account); const balance2Data = await api.query.assets.account(pool.asset2, account); setBalance1(balance1Data.isSome ? balance1Data.unwrap().balance.toString() : '0'); setBalance2(balance2Data.isSome ? balance2Data.unwrap().balance.toString() : '0'); } catch (error) { console.error('Failed to fetch balances:', error); } }; fetchBalances(); }, [api, isApiReady, account, pool]); // Auto-calculate amount2 when amount1 changes const handleAmount1Change = (value: string) => { setAmount1Input(value); if (!pool || !value || parseFloat(value) === 0) { setAmount2Input(''); return; } try { const amount1Raw = parseTokenInput(value, pool.asset1Decimals); const amount2Raw = quote(amount1Raw, pool.reserve2, pool.reserve1); const amount2Display = formatTokenBalance(amount2Raw, pool.asset2Decimals, 6); setAmount2Input(amount2Display); } catch (error) { console.error('Failed to calculate amount2:', error); } }; // Auto-calculate amount1 when amount2 changes const handleAmount2Change = (value: string) => { setAmount2Input(value); if (!pool || !value || parseFloat(value) === 0) { setAmount1Input(''); return; } try { const amount2Raw = parseTokenInput(value, pool.asset2Decimals); const amount1Raw = quote(amount2Raw, pool.reserve1, pool.reserve2); const amount1Display = formatTokenBalance(amount1Raw, pool.asset1Decimals, 6); setAmount1Input(amount1Display); } catch (error) { console.error('Failed to calculate amount1:', error); } }; const validateInputs = (): string | null => { if (!pool) return 'No pool selected'; if (!amount1Input || !amount2Input) return 'Please enter amounts'; const amount1Raw = parseTokenInput(amount1Input, pool.asset1Decimals); const amount2Raw = parseTokenInput(amount2Input, pool.asset2Decimals); if (BigInt(amount1Raw) <= BigInt(0) || BigInt(amount2Raw) <= BigInt(0)) { return 'Amounts must be greater than zero'; } if (BigInt(amount1Raw) > BigInt(balance1)) { return `Insufficient ${pool.asset1Symbol} balance`; } if (BigInt(amount2Raw) > BigInt(balance2)) { return `Insufficient ${pool.asset2Symbol} balance`; } return null; }; const handleAddLiquidity = async () => { if (!api || !isApiReady || !signer || !account || !pool) { setErrorMessage('Wallet not connected'); return; } const validationError = validateInputs(); if (validationError) { setErrorMessage(validationError); return; } const amount1Raw = parseTokenInput(amount1Input, pool.asset1Decimals); const amount2Raw = parseTokenInput(amount2Input, pool.asset2Decimals); // Calculate minimum amounts with slippage tolerance const minAmount1 = (BigInt(amount1Raw) * BigInt(100 - slippage * 100)) / BigInt(10000); const minAmount2 = (BigInt(amount2Raw) * BigInt(100 - slippage * 100)) / BigInt(10000); try { setTxStatus('signing'); setErrorMessage(''); const tx = api.tx.assetConversion.addLiquidity( pool.asset1, pool.asset2, amount1Raw, amount2Raw, minAmount1.toString(), minAmount2.toString(), account ); setTxStatus('submitting'); await tx.signAndSend( account, { signer }, ({ status, dispatchError }) => { if (status.isInBlock) { if (dispatchError) { if (dispatchError.isModule) { const decoded = api.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: any) { console.error('Add liquidity failed:', error); setErrorMessage(error.message || 'Transaction failed'); setTxStatus('error'); } }; if (!isOpen || !pool) return null; const shareOfPool = amount1Input && parseFloat(amount1Input) > 0 ? ( (parseFloat( formatTokenBalance( parseTokenInput(amount1Input, pool.asset1Decimals), pool.asset1Decimals, 6 ) ) / (parseFloat(formatTokenBalance(pool.reserve1, pool.asset1Decimals, 6)) + parseFloat( formatTokenBalance( parseTokenInput(amount1Input, pool.asset1Decimals), pool.asset1Decimals, 6 ) ))) * 100 ).toFixed(4) : '0'; return (
Add Liquidity
{pool.asset1Symbol} / {pool.asset2Symbol} Pool
{/* Info Banner */}
Add liquidity in proportion to the pool's current ratio. You'll receive LP tokens representing your share.
{/* Token 1 Input */}
Balance: {formatTokenBalance(balance1, pool.asset1Decimals, 4)}
handleAmount1Change(e.target.value)} placeholder="0.0" className="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg text-white text-lg placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500" disabled={txStatus === 'signing' || txStatus === 'submitting'} />
{/* Plus Icon */}
{/* Token 2 Input */}
Balance: {formatTokenBalance(balance2, pool.asset2Decimals, 4)}
handleAmount2Change(e.target.value)} placeholder="0.0" className="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg text-white text-lg placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500" disabled={txStatus === 'signing' || txStatus === 'submitting'} />
{/* Slippage Tolerance */}
{[0.5, 1, 2].map((value) => ( ))}
{/* Pool Share Preview */} {amount1Input && amount2Input && (
Share of Pool {shareOfPool}%
Exchange Rate 1 {pool.asset1Symbol} ={' '} {( parseFloat(formatTokenBalance(pool.reserve2, pool.asset2Decimals, 6)) / parseFloat(formatTokenBalance(pool.reserve1, pool.asset1Decimals, 6)) ).toFixed(6)}{' '} {pool.asset2Symbol}
)} {/* Error Message */} {errorMessage && (
{errorMessage}
)} {/* Success Message */} {txStatus === 'success' && (
Liquidity added successfully!
)} {/* Action Buttons */}
); };