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'; import { ASSET_IDS, getAssetSymbol } from '@pezkuwi/lib/wallet'; interface AddLiquidityModalProps { isOpen: boolean; onClose: () => void; asset0?: number; // Pool's first asset ID asset1?: number; // Pool's second asset ID } interface AssetDetails { minBalance?: string | number; } interface AssetAccountData { balance: string | number; } interface Balances { [key: string]: number; } // Helper to get display name (users see HEZ not wHEZ, PEZ, USDT not wUSDT) const getDisplayName = (assetId: number): string => { if (assetId === ASSET_IDS.WHEZ || assetId === 0) return 'HEZ'; if (assetId === ASSET_IDS.PEZ || assetId === 1) return 'PEZ'; if (assetId === ASSET_IDS.WUSDT || assetId === 1000) return 'USDT'; return getAssetSymbol(assetId); }; // Helper to get balance key for the asset const getBalanceKey = (assetId: number): string => { if (assetId === ASSET_IDS.WHEZ || assetId === 0) return 'HEZ'; if (assetId === ASSET_IDS.PEZ || assetId === 1) return 'PEZ'; if (assetId === ASSET_IDS.WUSDT || assetId === 1000) return 'USDT'; return getAssetSymbol(assetId); }; // Helper to get decimals for asset const getAssetDecimals = (assetId: number): number => { if (assetId === ASSET_IDS.WUSDT || assetId === 1000) return 6; // wUSDT has 6 decimals return 12; // wHEZ, PEZ have 12 decimals }; export const AddLiquidityModal: React.FC = ({ isOpen, onClose, asset0 = 0, // Default to wHEZ asset1 = 1 // Default to PEZ }) => { const { api, selectedAccount, isApiReady } = usePolkadot(); const { balances, refreshBalances } = useWallet(); const [amount0, setAmount0] = useState(''); const [amount1, setAmount1] = useState(''); const [currentPrice, setCurrentPrice] = useState(null); const [isPoolEmpty, setIsPoolEmpty] = useState(true); // Track if pool has meaningful liquidity const [minDeposit0, setMinDeposit0] = useState(0.01); // Dynamic minimum deposit for asset0 const [minDeposit1, setMinDeposit1] = useState(0.01); // Dynamic minimum deposit for asset1 const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(false); // Get asset details const asset0Name = getDisplayName(asset0); const asset1Name = getDisplayName(asset1); const asset0BalanceKey = getBalanceKey(asset0); const asset1BalanceKey = getBalanceKey(asset1); const asset0Decimals = getAssetDecimals(asset0); const asset1Decimals = getAssetDecimals(asset1); // Reset form when modal is closed useEffect(() => { if (!isOpen) { setAmount0(''); setAmount1(''); setError(null); setSuccess(false); } }, [isOpen]); // Fetch minimum deposit requirements from runtime useEffect(() => { if (!api || !isApiReady || !isOpen) return; const fetchMinimumBalances = async () => { try { // Query asset details which contains minBalance const assetDetails0 = await api.query.assets.asset(asset0); const assetDetails1 = await api.query.assets.asset(asset1); if (import.meta.env.DEV) console.log('🔍 Querying minimum balances for assets:', { asset0, asset1 }); if (assetDetails0.isSome && assetDetails1.isSome) { const details0 = assetDetails0.unwrap().toJSON() as AssetDetails; const details1 = assetDetails1.unwrap().toJSON() as AssetDetails; if (import.meta.env.DEV) console.log('📦 Asset details:', { asset0: details0, asset1: details1 }); const minBalance0Raw = details0.minBalance || '0'; const minBalance1Raw = details1.minBalance || '0'; const minBalance0 = Number(minBalance0Raw) / Math.pow(10, asset0Decimals); const minBalance1 = Number(minBalance1Raw) / Math.pow(10, asset1Decimals); if (import.meta.env.DEV) console.log('📊 Minimum deposit requirements from assets:', { asset0: asset0Name, minBalance0Raw, minBalance0, asset1: asset1Name, minBalance1Raw, minBalance1 }); setMinDeposit0(minBalance0); setMinDeposit1(minBalance1); } else { if (import.meta.env.DEV) console.warn('⚠️ Asset details not found, using defaults'); } // Also check if there's a MintMinLiquidity constant in assetConversion pallet if (api.consts.assetConversion) { const mintMinLiq = api.consts.assetConversion.mintMinLiquidity; if (mintMinLiq) { if (import.meta.env.DEV) console.log('🔧 AssetConversion MintMinLiquidity constant:', mintMinLiq.toString()); } const liquidityWithdrawalFee = api.consts.assetConversion.liquidityWithdrawalFee; if (liquidityWithdrawalFee) { if (import.meta.env.DEV) console.log('🔧 AssetConversion LiquidityWithdrawalFee:', liquidityWithdrawalFee.toHuman()); } // Log all assetConversion constants if (import.meta.env.DEV) console.log('🔧 All assetConversion constants:', Object.keys(api.consts.assetConversion)); } } catch (err) { if (import.meta.env.DEV) console.error('❌ Error fetching minimum balances:', err); // Keep default 0.01 if query fails } }; fetchMinimumBalances(); }, [api, isApiReady, isOpen, asset0, asset1, asset0Decimals, asset1Decimals, asset0Name, asset1Name]); // Fetch current pool price useEffect(() => { if (!api || !isApiReady || !isOpen) return; const fetchPoolPrice = async () => { try { const poolId = [asset0, asset1]; const poolInfo = await api.query.assetConversion.pools(poolId); if (poolInfo.isSome) { // Derive pool account using AccountIdConverter const { stringToU8a } = await import('@polkadot/util'); const { blake2AsU8a } = await import('@polkadot/util-crypto'); const PALLET_ID = stringToU8a('py/ascon'); const poolIdType = api.createType('(u32, u32)', [asset0, asset1]); const palletIdType = api.createType('[u8; 8]', PALLET_ID); const fullTuple = api.createType('([u8; 8], (u32, u32))', [palletIdType, poolIdType]); const accountHash = blake2AsU8a(fullTuple.toU8a(), 256); const poolAccountId = api.createType('AccountId32', accountHash); // Get reserves const balance0Data = await api.query.assets.account(asset0, poolAccountId); const balance1Data = await api.query.assets.account(asset1, poolAccountId); if (balance0Data.isSome && balance1Data.isSome) { const data0 = balance0Data.unwrap().toJSON() as AssetAccountData; const data1 = balance1Data.unwrap().toJSON() as AssetAccountData; const reserve0 = Number(data0.balance) / Math.pow(10, asset0Decimals); const reserve1 = Number(data1.balance) / Math.pow(10, asset1Decimals); // Consider pool empty if reserves are less than 1 token (dust amounts) const MINIMUM_LIQUIDITY = 1; if (reserve0 >= MINIMUM_LIQUIDITY && reserve1 >= MINIMUM_LIQUIDITY) { setCurrentPrice(reserve1 / reserve0); setIsPoolEmpty(false); if (import.meta.env.DEV) console.log('Pool has liquidity - auto-calculating ratio:', reserve1 / reserve0); } else { setCurrentPrice(null); setIsPoolEmpty(true); if (import.meta.env.DEV) console.log('Pool is empty or has dust only - manual input allowed'); } } else { // No reserves found - pool is empty setCurrentPrice(null); setIsPoolEmpty(true); if (import.meta.env.DEV) console.log('Pool is empty - manual input allowed'); } } else { // Pool doesn't exist yet - completely empty setCurrentPrice(null); setIsPoolEmpty(true); if (import.meta.env.DEV) console.log('Pool does not exist yet - manual input allowed'); } } catch (err) { if (import.meta.env.DEV) console.error('Error fetching pool price:', err); // On error, assume pool is empty to allow manual input setCurrentPrice(null); setIsPoolEmpty(true); } }; fetchPoolPrice(); }, [api, isApiReady, isOpen, asset0, asset1, asset0Decimals, asset1Decimals]); // Auto-calculate asset1 amount based on asset0 input (only if pool has liquidity) useEffect(() => { if (!isPoolEmpty && amount0 && currentPrice) { const calculated = parseFloat(amount0) * currentPrice; setAmount1(calculated.toFixed(asset1Decimals === 6 ? 2 : 4)); } else if (!amount0 && !isPoolEmpty) { setAmount1(''); } // If pool is empty, don't auto-calculate - let user input both amounts }, [amount0, currentPrice, asset1Decimals, isPoolEmpty]); const handleAddLiquidity = async () => { if (!api || !selectedAccount || !amount0 || !amount1) return; setIsLoading(true); setError(null); try { // Validate amounts if (parseFloat(amount0) <= 0 || parseFloat(amount1) <= 0) { setError('Please enter valid amounts'); setIsLoading(false); return; } // Check minimum deposit requirements from runtime if (parseFloat(amount0) < minDeposit0) { setError(`${asset0Name} amount must be at least ${minDeposit0.toFixed(asset0Decimals === 6 ? 2 : 4)} (minimum deposit requirement)`); setIsLoading(false); return; } if (parseFloat(amount1) < minDeposit1) { setError(`${asset1Name} amount must be at least ${minDeposit1.toFixed(asset1Decimals === 6 ? 2 : 4)} (minimum deposit requirement)`); setIsLoading(false); return; } const balance0 = (balances as Balances)[asset0BalanceKey] || 0; const balance1 = (balances as Balances)[asset1BalanceKey] || 0; if (parseFloat(amount0) > balance0) { setError(`Insufficient ${asset0Name} balance`); setIsLoading(false); return; } if (parseFloat(amount1) > balance1) { setError(`Insufficient ${asset1Name} balance`); setIsLoading(false); return; } // Get the signer from the extension const injector = await web3FromAddress(selectedAccount.address); // Convert amounts to proper decimals const amount0BN = BigInt(Math.floor(parseFloat(amount0) * Math.pow(10, asset0Decimals))); const amount1BN = BigInt(Math.floor(parseFloat(amount1) * Math.pow(10, asset1Decimals))); // Min amounts (90% of desired to account for slippage) const minAmount0BN = (amount0BN * BigInt(90)) / BigInt(100); const minAmount1BN = (amount1BN * BigInt(90)) / BigInt(100); // Build transaction(s) let tx; // If asset0 is HEZ (0), need to wrap it first if (asset0 === 0 || asset0 === ASSET_IDS.WHEZ) { const wrapTx = api.tx.tokenWrapper.wrap(amount0BN.toString()); const addLiquidityTx = api.tx.assetConversion.addLiquidity( asset0, asset1, amount0BN.toString(), amount1BN.toString(), minAmount0BN.toString(), minAmount1BN.toString(), selectedAccount.address ); // Batch wrap + add liquidity tx = api.tx.utility.batchAll([wrapTx, addLiquidityTx]); } else { // Direct add liquidity (no wrapping needed) tx = api.tx.assetConversion.addLiquidity( asset0, asset1, amount0BN.toString(), amount1BN.toString(), minAmount0BN.toString(), minAmount1BN.toString(), selectedAccount.address ); } await tx.signAndSend( selectedAccount.address, { signer: injector.signer }, ({ status, events, dispatchError }) => { if (status.isInBlock) { if (import.meta.env.DEV) console.log('Transaction in block:', status.asInBlock.toHex()); } else if (status.isFinalized) { if (import.meta.env.DEV) 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(' ')}`; if (import.meta.env.DEV) console.error('Dispatch error:', errorMessage); } else { errorMessage = dispatchError.toString(); if (import.meta.env.DEV) console.error('Dispatch error:', errorMessage); } } events.forEach(({ event }) => { if (api.events.system.ExtrinsicFailed.is(event)) { if (import.meta.env.DEV) console.error('ExtrinsicFailed event:', event.toHuman()); } }); setError(errorMessage); setIsLoading(false); } else { if (import.meta.env.DEV) console.log('Transaction successful'); setSuccess(true); setIsLoading(false); setAmount0(''); setAmount1(''); refreshBalances(); setTimeout(() => { setSuccess(false); onClose(); }, 2000); } } } ); } catch (err) { if (import.meta.env.DEV) console.error('Error adding liquidity:', err); setError(err instanceof Error ? err.message : 'Failed to add liquidity'); setIsLoading(false); } }; if (!isOpen) return null; const balance0 = (balances as Balances)[asset0BalanceKey] || 0; const balance1 = (balances as Balances)[asset1BalanceKey] || 0; return (

Add Liquidity

{error && ( {error} )} {success && ( Liquidity added successfully! )} {isPoolEmpty ? ( First Liquidity Provider: Pool is empty! You are setting the initial price ratio. Minimum deposit: {minDeposit0.toFixed(asset0Decimals === 6 ? 2 : 4)} {asset0Name} and {minDeposit1.toFixed(asset1Decimals === 6 ? 2 : 4)} {asset1Name}. {(asset0 === 0 || asset0 === ASSET_IDS.WHEZ) && ' Your HEZ will be automatically wrapped to wHEZ.'} ) : ( Add liquidity to earn 3% fees from all swaps. Amounts are auto-calculated based on current pool ratio. Minimum deposit: {minDeposit0.toFixed(asset0Decimals === 6 ? 2 : 4)} {asset0Name} and {minDeposit1.toFixed(asset1Decimals === 6 ? 2 : 4)} {asset1Name}. {(asset0 === 0 || asset0 === ASSET_IDS.WHEZ) && ' Your HEZ will be automatically wrapped to wHEZ.'} )}
{/* Asset 0 Input */}
setAmount0(e.target.value)} placeholder={`${minDeposit0.toFixed(asset0Decimals === 6 ? 2 : 4)} or more`} min={minDeposit0} step={minDeposit0 < 1 ? minDeposit0 : 0.01} 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} />
{asset0Name}
Balance: {balance0.toLocaleString()}
{/* Asset 1 Input */}
setAmount1(e.target.value)} placeholder={isPoolEmpty ? `${minDeposit1.toFixed(asset1Decimals === 6 ? 2 : 4)} or more` : "Auto-calculated"} min={isPoolEmpty ? minDeposit1 : undefined} step={isPoolEmpty ? (minDeposit1 < 1 ? minDeposit1 : 0.01) : undefined} className={`w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 focus:outline-none ${ isPoolEmpty ? 'text-white focus:border-blue-500' : 'text-gray-400 cursor-not-allowed' }`} disabled={!isPoolEmpty || isLoading} readOnly={!isPoolEmpty} />
{asset1Name}
Balance: {balance1.toLocaleString()} {isPoolEmpty ? ( ) : ( currentPrice && Rate: 1 {asset0Name} = {currentPrice.toFixed(asset1Decimals === 6 ? 2 : 4)} {asset1Name} )}
{/* Price Info */} {amount0 && amount1 && (
{isPoolEmpty && (
Initial Price 1 {asset0Name} = {(parseFloat(amount1) / parseFloat(amount0)).toFixed(asset1Decimals === 6 ? 2 : 4)} {asset1Name}
)}
Share of Pool {isPoolEmpty ? '100%' : '~0.1%'}
Slippage Tolerance 10%
)}
); };