import React, { useState, useEffect, useCallback } from 'react'; import { usePezkuwi } from '@/contexts/PezkuwiContext'; import { useWallet } from '@/contexts/WalletContext'; import { X, Plus, AlertCircle, Loader2, CheckCircle } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { KNOWN_TOKENS, NATIVE_TOKEN_ID } from '@/types/dex'; import { parseTokenInput, formatTokenBalance } from '@pezkuwi/utils/dex'; import { useTranslation } from 'react-i18next'; interface CreatePoolModalProps { isOpen: boolean; onClose: () => void; onSuccess?: () => void; } type TransactionStatus = 'idle' | 'signing' | 'submitting' | 'success' | 'error'; export const CreatePoolModal: React.FC = ({ isOpen, onClose, onSuccess, }) => { // Use Asset Hub API for DEX operations (assetConversion pallet is on Asset Hub) const { assetHubApi, isAssetHubReady } = usePezkuwi(); const { account, signer } = useWallet(); const { t } = useTranslation(); const [asset1Id, setAsset1Id] = useState(null); const [asset2Id, setAsset2Id] = useState(null); const [amount1Input, setAmount1Input] = useState(''); const [amount2Input, setAmount2Input] = useState(''); const [balance1, setBalance1] = useState('0'); const [balance2, setBalance2] = useState('0'); const [txStatus, setTxStatus] = useState('idle'); const [errorMessage, setErrorMessage] = useState(''); // Available tokens const availableTokens = Object.values(KNOWN_TOKENS); // Reset form when modal closes useEffect(() => { if (!isOpen) { setAsset1Id(null); setAsset2Id(null); setAmount1Input(''); setAmount2Input(''); setTxStatus('idle'); setErrorMessage(''); } }, [isOpen]); // Helper to fetch balance for an asset (handles Native vs Asset) const fetchAssetBalance = useCallback(async (assetId: number): Promise => { if (!assetHubApi || !isAssetHubReady || !account) return '0'; try { if (assetId === NATIVE_TOKEN_ID) { // Native token - query system.account const accountData: { data: { free: { toString: () => string } } } = await assetHubApi.query.system.account(account) as never; return accountData.data.free.toString(); } else { // Asset - query assets.account const balanceData = await assetHubApi.query.assets.account(assetId, account); if ((balanceData as { isSome: boolean }).isSome) { return ((balanceData as { unwrap: () => { balance: { toString: () => string } } }).unwrap()).balance.toString(); } return '0'; } } catch (error) { if (import.meta.env.DEV) console.error('❌ Failed to fetch balance for asset', assetId, ':', error); return '0'; } }, [assetHubApi, isAssetHubReady, account]); // Fetch balances from Asset Hub when assets selected useEffect(() => { const fetchBalances = async () => { if (asset1Id === null) return; if (import.meta.env.DEV) console.log('🔍 Fetching balance for asset', asset1Id, 'on Asset Hub'); const balance = await fetchAssetBalance(asset1Id); if (import.meta.env.DEV) console.log('✅ Balance for asset', asset1Id, ':', balance); setBalance1(balance); }; fetchBalances(); }, [fetchAssetBalance, asset1Id]); useEffect(() => { const fetchBalances = async () => { if (asset2Id === null) return; if (import.meta.env.DEV) console.log('🔍 Fetching balance for asset', asset2Id, 'on Asset Hub'); const balance = await fetchAssetBalance(asset2Id); if (import.meta.env.DEV) console.log('✅ Balance for asset', asset2Id, ':', balance); setBalance2(balance); }; fetchBalances(); }, [fetchAssetBalance, asset2Id]); const validateInputs = (): string | null => { if (asset1Id === null || asset2Id === null) { return t('createPool.selectBothTokens'); } if (asset1Id === asset2Id) { return t('createPool.sameToken'); } if (!amount1Input || !amount2Input) { return t('createPool.enterBothAmounts'); } const token1 = KNOWN_TOKENS[asset1Id]; const token2 = KNOWN_TOKENS[asset2Id]; if (!token1 || !token2) { return t('createPool.invalidToken'); } const amount1Raw = parseTokenInput(amount1Input, token1.decimals); const amount2Raw = parseTokenInput(amount2Input, token2.decimals); if (import.meta.env.DEV) console.log('💰 Validation check:', { token1: token1.symbol, amount1Input, amount1Raw, balance1, hasEnough1: BigInt(amount1Raw) <= BigInt(balance1), token2: token2.symbol, amount2Input, amount2Raw, balance2, hasEnough2: BigInt(amount2Raw) <= BigInt(balance2), }); if (BigInt(amount1Raw) <= BigInt(0) || BigInt(amount2Raw) <= BigInt(0)) { return t('common.amountGtZero'); } if (BigInt(amount1Raw) > BigInt(balance1)) { return t('common.insufficientBalance', { symbol: token1.symbol }); } if (BigInt(amount2Raw) > BigInt(balance2)) { return t('common.insufficientBalance', { symbol: token2.symbol }); } return null; }; const handleCreatePool = async () => { if (!assetHubApi || !isAssetHubReady || !signer || !account) { setErrorMessage(t('createPool.walletNotReady')); return; } // Check if assetConversion pallet is available on Asset Hub if (!assetHubApi.tx.assetConversion || !assetHubApi.tx.assetConversion.createPool) { setErrorMessage(t('createPool.palletNotAvailable')); return; } const validationError = validateInputs(); if (validationError) { setErrorMessage(validationError); return; } const token1 = KNOWN_TOKENS[asset1Id!]; const token2 = KNOWN_TOKENS[asset2Id!]; const amount1Raw = parseTokenInput(amount1Input, token1.decimals); const amount2Raw = parseTokenInput(amount2Input, token2.decimals); try { setTxStatus('signing'); setErrorMessage(''); // Convert asset IDs to proper format for assetConversion pallet // Native token (relay chain HEZ) uses XCM location format // Assets use { Asset: id } const formatAssetId = (id: number) => { if (id === NATIVE_TOKEN_ID) { // Native token from relay chain - XCM location format // { parents: 1, interior: Here } represents relay chain native token return { parents: 1, interior: 'Here' }; } return { parents: 0, interior: { X2: [{ PalletInstance: 50 }, { GeneralIndex: id }] } }; }; const asset1 = formatAssetId(asset1Id!); const asset2 = formatAssetId(asset2Id!); if (import.meta.env.DEV) { console.log('🏊 Creating pool with:', { asset1, asset2, amount1Raw, amount2Raw }); } // Create pool extrinsic on Asset Hub const createPoolTx = assetHubApi.tx.assetConversion.createPool(asset1, asset2); // Add liquidity extrinsic on Asset Hub const addLiquidityTx = assetHubApi.tx.assetConversion.addLiquidity( asset1, asset2, amount1Raw, amount2Raw, amount1Raw, // min amount1 amount2Raw, // min amount2 account ); // Batch transactions const batchTx = assetHubApi.tx.utility.batchAll([createPoolTx, addLiquidityTx]); setTxStatus('submitting'); await batchTx.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('Pool creation failed:', error); setErrorMessage(error instanceof Error ? error.message : t('common.txFailed')); setTxStatus('error'); } }; if (!isOpen) return null; const token1 = asset1Id !== null ? KNOWN_TOKENS[asset1Id] : null; const token2 = asset2Id !== null ? KNOWN_TOKENS[asset2Id] : null; const exchangeRate = amount1Input && amount2Input && parseFloat(amount1Input) > 0 ? (parseFloat(amount2Input) / parseFloat(amount1Input)).toFixed(6) : '0'; return (
{t('createPool.title')}
{t('createPool.founderOnly')}
{/* Token 1 Selection */}
{token1 && (
{t('common.balance')}: {formatTokenBalance(balance1, token1.decimals, 4)} {token1.symbol}
)}
{/* Amount 1 Input */} {token1 && (
setAmount1Input(e.target.value)} placeholder="0.0" className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500" disabled={txStatus === 'signing' || txStatus === 'submitting'} />
)} {/* Plus Icon */}
{/* Token 2 Selection */}
{token2 && (
{t('common.balance')}: {formatTokenBalance(balance2, token2.decimals, 4)} {token2.symbol}
)}
{/* Amount 2 Input */} {token2 && (
setAmount2Input(e.target.value)} placeholder="0.0" className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500" disabled={txStatus === 'signing' || txStatus === 'submitting'} />
)} {/* Exchange Rate Preview */} {token1 && token2 && amount1Input && amount2Input && (
{t('createPool.initialRate')}
1 {token1.symbol} = {exchangeRate} {token2.symbol}
)} {/* Error Message */} {errorMessage && (
{errorMessage}
)} {/* Success Message */} {txStatus === 'success' && (
{t('createPool.success')}
)} {/* Action Buttons */}
); };