Files
pezkuwi-telegram-miniapp/src/components/wallet/PoolsModal.tsx
T
pezkuwichain 2017ae77da chore: eliminate all ESLint warnings, enforce --max-warnings 0
Behavior-preserving lint cleanup of the 30 pre-existing warnings, fixed
by category (no blanket suppression):
- no-explicit-any (19): precise local types / ApiPromise for Substrate
  dynamic queries + 'as unknown as' casts (same pattern as TokensCard).
- no-console (4): dev-gated (import.meta.env.DEV) / warn; main.tsx prod
  console-suppression kept with a scoped documented disable.
- no-non-null-assertion (4): replaced '!' with explicit guards reusing
  the existing fallback/return paths.
- react-hooks/exhaustive-deps (3): missing dep is 't' (i18n) — adding it
  would re-fire blockchain fetches on language change; kept deps with a
  documented intentional disable.

Tighten the lint gate from --max-warnings 30 to 0 so no new warnings can
land. Verified: tsc 0 errors, eslint --max-warnings 0 clean, vite build ok.
2026-06-14 09:21:12 -07:00

876 lines
33 KiB
TypeScript

/**
* Pools Modal Component
* Mobile-optimized liquidity pools interface for Telegram miniapp
*/
import { useState, useEffect } from 'react';
import { X, Droplets, Plus, Minus, AlertCircle, Check } from 'lucide-react';
import { useWallet } from '@/contexts/WalletContext';
import { useTelegram } from '@/hooks/useTelegram';
import { KurdistanSun } from '@/components/KurdistanSun';
import { useTranslation } from '@/i18n';
interface PoolsModalProps {
isOpen: boolean;
onClose: () => void;
}
interface Pool {
id: string;
asset0: number;
asset1: number;
asset0Symbol: string;
asset1Symbol: string;
asset0Decimals: number;
asset1Decimals: number;
lpTokenId: number;
reserve0: number;
reserve1: number;
price: number;
userLpBalance?: number;
userShare?: number;
}
// Native token ID
const NATIVE_TOKEN_ID = -1;
// Token info mapping
const TOKEN_INFO: Record<number, { symbol: string; decimals: number; icon: string }> = {
[-1]: { symbol: 'HEZ', decimals: 12, icon: '/tokens/HEZ.png' },
1: { symbol: 'PEZ', decimals: 12, icon: '/tokens/PEZ.png' },
1000: { symbol: 'USDT', decimals: 6, icon: '/tokens/USDT.png' },
1001: { symbol: 'DOT', decimals: 10, icon: '/tokens/DOT.png' },
};
// Helper to convert asset ID to XCM Location format
const formatAssetLocation = (id: number) => {
if (id === NATIVE_TOKEN_ID) {
return { parents: 1, interior: 'Here' };
}
return { parents: 0, interior: { X2: [{ PalletInstance: 50 }, { GeneralIndex: id }] } };
};
// Minimal shapes for the dynamic @pezkuwi/api results we read here. These
// pallets/runtime-calls are not in the base typings, so we describe just the
// fields actually used and cast via `as unknown as`.
type AccountInfoResult = { data: { free: { toString(): string } } };
type AssetAccountResult = {
isSome: boolean;
unwrap(): { balance: { toString(): string } };
};
type QuoteResult = { isNone: boolean; unwrap(): { toString(): string } };
type AssetConversionCall = {
assetConversionApi: {
quotePriceExactTokensForTokens: (
asset0: ReturnType<typeof formatAssetLocation>,
asset1: ReturnType<typeof formatAssetLocation>,
amount: string,
includeFee: boolean
) => Promise<QuoteResult>;
};
};
export function PoolsModal({ isOpen, onClose }: PoolsModalProps) {
const { assetHubApi, keypair } = useWallet();
const { hapticImpact, hapticNotification } = useTelegram();
const { t } = useTranslation();
const [pools, setPools] = useState<Pool[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [selectedPool, setSelectedPool] = useState<Pool | null>(null);
const [isAddingLiquidity, setIsAddingLiquidity] = useState(false);
const [isRemovingLiquidity, setIsRemovingLiquidity] = useState(false);
const [amount0, setAmount0] = useState('');
const [amount1, setAmount1] = useState('');
const [lpAmountToRemove, setLpAmountToRemove] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState(false);
const [successMessage, setSuccessMessage] = useState('');
// Token balances
const [balances, setBalances] = useState<Record<string, string>>({
HEZ: '0',
PEZ: '0',
USDT: '0',
});
// Fetch balances and pools
useEffect(() => {
if (!isOpen || !assetHubApi || !keypair) return;
// Reset state when modal opens
setError('');
setAmount0('');
setAmount1('');
setLpAmountToRemove('');
let isCancelled = false;
const fetchData = async () => {
setIsLoading(true);
try {
// Add timeout wrapper for API calls
const withTimeout = <T,>(promise: Promise<T>, ms: number): Promise<T> => {
return Promise.race([
promise,
new Promise<T>((_, reject) =>
setTimeout(() => reject(new Error('API call timeout')), ms)
),
]);
};
// Fetch HEZ balance from Asset Hub (native token)
const hezAccount = (await withTimeout(
assetHubApi.query.system.account(keypair.address),
10000
)) as unknown as AccountInfoResult;
if (isCancelled) return;
const hezFree = hezAccount.data.free.toString();
setBalances((prev) => ({ ...prev, HEZ: (parseInt(hezFree) / 1e12).toFixed(4) }));
const pezResult = (await withTimeout(
assetHubApi.query.assets.account(1, keypair.address),
10000
)) as unknown as AssetAccountResult;
if (isCancelled) return;
if (pezResult.isSome) {
setBalances((prev) => ({
...prev,
PEZ: (parseInt(pezResult.unwrap().balance.toString()) / 1e12).toFixed(4),
}));
} else {
setBalances((prev) => ({ ...prev, PEZ: '0.0000' }));
}
const usdtResult = (await withTimeout(
assetHubApi.query.assets.account(1000, keypair.address),
10000
)) as unknown as AssetAccountResult;
if (isCancelled) return;
if (usdtResult.isSome) {
setBalances((prev) => ({
...prev,
USDT: (parseInt(usdtResult.unwrap().balance.toString()) / 1e6).toFixed(2),
}));
} else {
setBalances((prev) => ({ ...prev, USDT: '0.00' }));
}
// Fetch pools
const poolPairs: [number, number][] = [
[NATIVE_TOKEN_ID, 1], // HEZ-PEZ
[NATIVE_TOKEN_ID, 1000], // HEZ-USDT
[NATIVE_TOKEN_ID, 1001], // HEZ-DOT
];
const fetchedPools: Pool[] = [];
for (const [asset0, asset1] of poolPairs) {
if (isCancelled) return;
try {
const poolKey = [formatAssetLocation(asset0), formatAssetLocation(asset1)];
const poolInfo = await withTimeout(
assetHubApi.query.assetConversion.pools(poolKey),
10000
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (poolInfo && !(poolInfo as any).isEmpty) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const poolData = (poolInfo as any).unwrap().toJSON() as { lpToken: number };
const lpTokenId = poolData.lpToken;
const token0 = TOKEN_INFO[asset0];
const token1 = TOKEN_INFO[asset1];
// Get price quote
let price = 0;
let reserve0 = 0;
let reserve1 = 0;
try {
const oneUnit = BigInt(Math.pow(10, token0.decimals));
const quote = await withTimeout(
(
assetHubApi.call as unknown as AssetConversionCall
).assetConversionApi.quotePriceExactTokensForTokens(
formatAssetLocation(asset0),
formatAssetLocation(asset1),
oneUnit.toString(),
true
),
10000
);
if (quote && !quote.isNone) {
price = Number(BigInt(quote.unwrap().toString())) / Math.pow(10, token1.decimals);
// Estimate reserves from LP supply
const lpAsset = await withTimeout(
assetHubApi.query.poolAssets.asset(lpTokenId),
10000
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((lpAsset as any).isSome) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const lpSupply = Number((lpAsset as any).unwrap().toJSON().supply) / 1e12;
// Decimal correction factor for mixed-decimal pools
const decimalFactor = Math.pow(
10,
12 - (token0.decimals + token1.decimals) / 2
);
const sqrtPrice = Math.sqrt(price);
reserve0 = (lpSupply * decimalFactor) / sqrtPrice;
reserve1 = lpSupply * decimalFactor * sqrtPrice;
}
}
} catch (err) {
console.warn('Could not fetch price for pool:', err);
}
// Get user's LP balance
let userLpBalance = 0;
let userShare = 0;
try {
const userLp = await withTimeout(
assetHubApi.query.poolAssets.account(lpTokenId, keypair.address),
10000
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((userLp as any).isSome) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
userLpBalance = Number((userLp as any).unwrap().toJSON().balance) / 1e12;
const lpAsset = await withTimeout(
assetHubApi.query.poolAssets.asset(lpTokenId),
10000
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((lpAsset as any).isSome) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const totalSupply = Number((lpAsset as any).unwrap().toJSON().supply) / 1e12;
userShare = totalSupply > 0 ? (userLpBalance / totalSupply) * 100 : 0;
}
}
} catch (err) {
console.warn('Could not fetch user LP balance:', err);
}
fetchedPools.push({
id: `${asset0}-${asset1}`,
asset0,
asset1,
asset0Symbol: token0.symbol,
asset1Symbol: token1.symbol,
asset0Decimals: token0.decimals,
asset1Decimals: token1.decimals,
lpTokenId,
reserve0,
reserve1,
price,
userLpBalance,
userShare,
});
}
} catch (err) {
console.warn('Pool not found:', err);
}
}
if (!isCancelled) {
setPools(fetchedPools);
}
} catch (err) {
console.error('Failed to fetch pools:', err);
if (!isCancelled) {
setError(t('pools.connectionError'));
}
} finally {
if (!isCancelled) {
setIsLoading(false);
}
}
};
fetchData();
return () => {
isCancelled = true;
};
// `t` is intentionally omitted: it is only used for error messages and its
// identity changes only on language switch — including it would re-run the
// blockchain fetch (extra RPC calls) on every language change.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen, assetHubApi, keypair]);
// Auto-calculate amount1 based on pool price
useEffect(() => {
if (selectedPool && amount0 && selectedPool.price > 0) {
const calculated = parseFloat(amount0) * selectedPool.price;
setAmount1(calculated.toFixed(selectedPool.asset1Decimals === 6 ? 2 : 4));
} else {
setAmount1('');
}
}, [amount0, selectedPool]);
// Add liquidity
const handleAddLiquidity = async () => {
if (!assetHubApi || !keypair || !selectedPool || !amount0 || !amount1) return;
setIsSubmitting(true);
setError('');
try {
const amt0 = BigInt(
Math.floor(parseFloat(amount0) * Math.pow(10, selectedPool.asset0Decimals))
);
const amt1 = BigInt(
Math.floor(parseFloat(amount1) * Math.pow(10, selectedPool.asset1Decimals))
);
const minAmt0 = (amt0 * BigInt(90)) / BigInt(100); // 10% slippage
const minAmt1 = (amt1 * BigInt(90)) / BigInt(100);
const asset0Location = formatAssetLocation(selectedPool.asset0);
const asset1Location = formatAssetLocation(selectedPool.asset1);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const tx = (assetHubApi.tx.assetConversion as any).addLiquidity(
asset0Location,
asset1Location,
amt0.toString(),
amt1.toString(),
minAmt0.toString(),
minAmt1.toString(),
keypair.address
);
// Minimal shape of the signAndSend result fields we read. `asModule`
// is typed from the registry call so it stays type-safe.
type TxResult = {
status: { isFinalized: boolean };
dispatchError?: {
isModule: boolean;
asModule: Parameters<typeof assetHubApi.registry.findMetaError>[0];
toString(): string;
};
};
// Wait for transaction to be finalized
await new Promise<void>((resolve, reject) => {
tx.signAndSend(keypair, ({ status, dispatchError }: TxResult) => {
if (status.isFinalized) {
if (dispatchError) {
let errorMsg = t('pools.addFailed');
if (dispatchError.isModule) {
const decoded = assetHubApi.registry.findMetaError(dispatchError.asModule);
errorMsg = `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`;
} else if (dispatchError.toString) {
errorMsg = dispatchError.toString();
}
console.error('Add liquidity error:', errorMsg);
reject(new Error(errorMsg));
} else {
resolve();
}
}
}).catch(reject);
});
setSuccessMessage(
t('pools.addedLiquidity', {
amount0,
token0: selectedPool.asset0Symbol,
amount1,
token1: selectedPool.asset1Symbol,
})
);
setSuccess(true);
hapticNotification('success');
setTimeout(() => {
setSuccess(false);
setIsAddingLiquidity(false);
setSelectedPool(null);
setAmount0('');
setAmount1('');
}, 2000);
} catch (err) {
console.error('Add liquidity failed:', err);
setError(err instanceof Error ? err.message : t('pools.addFailed'));
hapticNotification('error');
} finally {
setIsSubmitting(false);
}
};
// Remove liquidity
const handleRemoveLiquidity = async () => {
if (!assetHubApi || !keypair || !selectedPool || !lpAmountToRemove) return;
const lpAmount = parseFloat(lpAmountToRemove);
if (lpAmount <= 0 || lpAmount > (selectedPool.userLpBalance || 0)) {
setError(t('pools.invalidLpAmount'));
hapticNotification('error');
return;
}
setIsSubmitting(true);
setError('');
try {
const lpAmountRaw = BigInt(Math.floor(lpAmount * 1e12));
// Calculate minimum amounts to receive (with 10% slippage)
const userShare =
((lpAmount / (selectedPool.userLpBalance || 1)) * (selectedPool.userShare || 0)) / 100;
const expectedAmt0 = selectedPool.reserve0 * userShare;
const expectedAmt1 = selectedPool.reserve1 * userShare;
const minAmt0 = BigInt(
Math.floor(expectedAmt0 * 0.9 * Math.pow(10, selectedPool.asset0Decimals))
);
const minAmt1 = BigInt(
Math.floor(expectedAmt1 * 0.9 * Math.pow(10, selectedPool.asset1Decimals))
);
const asset0Location = formatAssetLocation(selectedPool.asset0);
const asset1Location = formatAssetLocation(selectedPool.asset1);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const tx = (assetHubApi.tx.assetConversion as any).removeLiquidity(
asset0Location,
asset1Location,
lpAmountRaw.toString(),
minAmt0.toString(),
minAmt1.toString(),
keypair.address
);
// Minimal shape of the signAndSend result fields we read. `asModule`
// is typed from the registry call so it stays type-safe.
type TxResult = {
status: { isFinalized: boolean };
dispatchError?: {
isModule: boolean;
asModule: Parameters<typeof assetHubApi.registry.findMetaError>[0];
toString(): string;
};
};
// Wait for transaction to be finalized
await new Promise<void>((resolve, reject) => {
tx.signAndSend(keypair, ({ status, dispatchError }: TxResult) => {
if (status.isFinalized) {
if (dispatchError) {
let errorMsg = t('pools.removeFailed');
if (dispatchError.isModule) {
const decoded = assetHubApi.registry.findMetaError(dispatchError.asModule);
errorMsg = `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`;
} else if (dispatchError.toString) {
errorMsg = dispatchError.toString();
}
console.error('Remove liquidity error:', errorMsg);
reject(new Error(errorMsg));
} else {
resolve();
}
}
}).catch(reject);
});
setSuccessMessage(t('pools.removedLiquidity', { amount: lpAmountToRemove }));
setSuccess(true);
hapticNotification('success');
setTimeout(() => {
setSuccess(false);
setIsRemovingLiquidity(false);
setSelectedPool(null);
setLpAmountToRemove('');
}, 2000);
} catch (err) {
console.error('Remove liquidity failed:', err);
setError(err instanceof Error ? err.message : t('pools.removeFailed'));
hapticNotification('error');
} finally {
setIsSubmitting(false);
}
};
if (!isOpen) return null;
// Success screen
if (success) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm">
<div className="w-full max-w-md bg-card rounded-2xl p-6 text-center space-y-4">
<div className="w-16 h-16 mx-auto bg-green-500/20 rounded-full flex items-center justify-center">
<Check className="w-8 h-8 text-green-500" />
</div>
<h2 className="text-xl font-semibold">{t('pools.success')}</h2>
<p className="text-muted-foreground">{successMessage}</p>
</div>
</div>
);
}
// Add liquidity form
if (isAddingLiquidity && selectedPool) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm">
<div className="w-full max-w-md bg-card rounded-2xl shadow-xl border border-border overflow-hidden max-h-[90vh] overflow-y-auto">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-border">
<button
onClick={() => {
setIsAddingLiquidity(false);
setAmount0('');
setAmount1('');
setError('');
}}
className="text-muted-foreground"
>
{t('pools.back')}
</button>
<h2 className="text-lg font-semibold">{t('pools.addLiquidity')}</h2>
<div className="w-10" />
</div>
{/* Pool Info */}
<div className="p-4 bg-muted/30 border-b border-border">
<div className="flex items-center justify-center gap-2">
<span className="text-lg font-semibold">
{selectedPool.asset0Symbol}/{selectedPool.asset1Symbol}
</span>
</div>
<p className="text-center text-sm text-muted-foreground mt-1">
1 {selectedPool.asset0Symbol} = {selectedPool.price.toFixed(4)}{' '}
{selectedPool.asset1Symbol}
</p>
</div>
{/* Form */}
<div className="p-4 space-y-4">
{/* Amount 0 */}
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">{selectedPool.asset0Symbol} Mîqdar</span>
<span className="text-muted-foreground">
{t('swap.balanceLabel')} {balances[selectedPool.asset0Symbol]}
</span>
</div>
<div className="flex gap-2">
<input
type="number"
value={amount0}
onChange={(e) => setAmount0(e.target.value)}
placeholder="0.00"
className="flex-1 px-4 py-3 bg-muted rounded-xl text-lg font-mono"
/>
<button
onClick={() => setAmount0(balances[selectedPool.asset0Symbol])}
className="px-3 py-2 bg-muted rounded-xl text-sm text-primary"
>
Max
</button>
</div>
</div>
<div className="flex justify-center">
<Plus className="w-5 h-5 text-muted-foreground" />
</div>
{/* Amount 1 */}
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">
{t('pools.amountAuto', { token: selectedPool.asset1Symbol })}
</span>
<span className="text-muted-foreground">
{t('swap.balanceLabel')} {balances[selectedPool.asset1Symbol]}
</span>
</div>
<input
type="text"
value={amount1}
readOnly
placeholder="0.00"
className="w-full px-4 py-3 bg-muted rounded-xl text-lg font-mono text-muted-foreground"
/>
</div>
{/* Error */}
{error && (
<div className="flex items-center gap-2 p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm">
<AlertCircle className="w-4 h-4" />
{error}
</div>
)}
{/* Submit Button or Loading Animation */}
{isSubmitting ? (
<div className="flex flex-col items-center justify-center py-4 space-y-3">
<KurdistanSun size={80} />
<p className="text-sm text-muted-foreground animate-pulse">{t('pools.adding')}</p>
</div>
) : (
<button
onClick={handleAddLiquidity}
disabled={!amount0 || !amount1 || parseFloat(amount0) <= 0}
className="w-full py-4 bg-gradient-to-r from-green-600 to-blue-600 text-white font-semibold rounded-xl disabled:opacity-50 flex items-center justify-center gap-2"
>
<Droplets className="w-5 h-5" />
{t('pools.addLiquidity')}
</button>
)}
</div>
</div>
</div>
);
}
// Remove liquidity form
if (isRemovingLiquidity && selectedPool) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm">
<div className="w-full max-w-md bg-card rounded-2xl shadow-xl border border-border overflow-hidden max-h-[90vh] overflow-y-auto">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-border">
<button
onClick={() => {
setIsRemovingLiquidity(false);
setLpAmountToRemove('');
setError('');
}}
className="text-muted-foreground"
>
{t('pools.back')}
</button>
<h2 className="text-lg font-semibold">{t('pools.removeLiquidity')}</h2>
<div className="w-10" />
</div>
{/* Pool Info */}
<div className="p-4 bg-muted/30 border-b border-border">
<div className="flex items-center justify-center gap-2">
<span className="text-lg font-semibold">
{selectedPool.asset0Symbol}/{selectedPool.asset1Symbol}
</span>
</div>
<p className="text-center text-sm text-muted-foreground mt-1">
{t('pools.lpBalance')} {selectedPool.userLpBalance?.toFixed(4) || '0'} LP
</p>
</div>
{/* Form */}
<div className="p-4 space-y-4">
{/* LP Amount */}
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">{t('pools.lpTokenAmount')}</span>
<span className="text-muted-foreground">
Max: {selectedPool.userLpBalance?.toFixed(4) || '0'}
</span>
</div>
<div className="flex gap-2">
<input
type="number"
value={lpAmountToRemove}
onChange={(e) => setLpAmountToRemove(e.target.value)}
placeholder="0.00"
className="flex-1 px-4 py-3 bg-muted rounded-xl text-lg font-mono"
/>
<button
onClick={() => setLpAmountToRemove(selectedPool.userLpBalance?.toString() || '0')}
className="px-3 py-2 bg-muted rounded-xl text-sm text-primary"
>
Max
</button>
</div>
</div>
{/* Estimated Returns */}
{lpAmountToRemove && parseFloat(lpAmountToRemove) > 0 && (
<div className="bg-muted/50 rounded-xl p-3 space-y-2 text-sm">
<p className="text-muted-foreground">{t('pools.estimatedReturn')}</p>
<div className="flex justify-between">
<span>{selectedPool.asset0Symbol}</span>
<span className="font-mono">
~
{(
(((parseFloat(lpAmountToRemove) / (selectedPool.userLpBalance || 1)) *
(selectedPool.userShare || 0)) /
100) *
selectedPool.reserve0
).toFixed(4)}
</span>
</div>
<div className="flex justify-between">
<span>{selectedPool.asset1Symbol}</span>
<span className="font-mono">
~
{(
(((parseFloat(lpAmountToRemove) / (selectedPool.userLpBalance || 1)) *
(selectedPool.userShare || 0)) /
100) *
selectedPool.reserve1
).toFixed(selectedPool.asset1Decimals === 6 ? 2 : 4)}
</span>
</div>
</div>
)}
{/* Error */}
{error && (
<div className="flex items-center gap-2 p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm">
<AlertCircle className="w-4 h-4" />
{error}
</div>
)}
{/* Submit Button or Loading Animation */}
{isSubmitting ? (
<div className="flex flex-col items-center justify-center py-4 space-y-3">
<KurdistanSun size={80} />
<p className="text-sm text-muted-foreground animate-pulse">{t('pools.removing')}</p>
</div>
) : (
<button
onClick={handleRemoveLiquidity}
disabled={!lpAmountToRemove || parseFloat(lpAmountToRemove) <= 0}
className="w-full py-4 bg-gradient-to-r from-red-600 to-orange-600 text-white font-semibold rounded-xl disabled:opacity-50 flex items-center justify-center gap-2"
>
<Minus className="w-5 h-5" />
{t('pools.removeLiquidity')}
</button>
)}
</div>
</div>
</div>
);
}
// Pool list
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm">
<div className="w-full max-w-md bg-card rounded-2xl shadow-xl border border-border overflow-hidden max-h-[90vh] overflow-y-auto">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-border">
<h2 className="text-lg font-semibold">{t('pools.title')}</h2>
<button onClick={onClose} className="p-2 rounded-full hover:bg-muted">
<X className="w-5 h-5 text-muted-foreground" />
</button>
</div>
{/* Content */}
<div className="p-4 space-y-3">
{isLoading ? (
<div className="flex flex-col items-center justify-center py-8">
<KurdistanSun size={80} />
<p className="text-muted-foreground mt-3 animate-pulse">{t('pools.loadingPools')}</p>
</div>
) : pools.length === 0 ? (
<div className="text-center py-8">
<Droplets className="w-12 h-12 mx-auto text-muted-foreground mb-2" />
<p className="text-muted-foreground">{t('pools.noPools')}</p>
</div>
) : (
pools.map((pool) => (
<div key={pool.id} className="bg-muted/50 rounded-xl p-4 border border-border">
{/* Pool Header */}
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div className="flex -space-x-2">
<img
src={TOKEN_INFO[pool.asset0]?.icon}
alt={pool.asset0Symbol}
className="w-8 h-8 rounded-full border-2 border-card"
/>
<img
src={TOKEN_INFO[pool.asset1]?.icon}
alt={pool.asset1Symbol}
className="w-8 h-8 rounded-full border-2 border-card"
/>
</div>
<span className="font-semibold">
{pool.asset0Symbol}/{pool.asset1Symbol}
</span>
</div>
</div>
{/* Pool Stats */}
<div className="grid grid-cols-2 gap-2 text-sm mb-3">
<div>
<span className="text-muted-foreground">
{t('pools.reserve')} {pool.asset0Symbol}
</span>
<p className="font-mono">
{pool.reserve0.toLocaleString('en-US', { maximumFractionDigits: 0 })}
</p>
</div>
<div>
<span className="text-muted-foreground">
{t('pools.reserve')} {pool.asset1Symbol}
</span>
<p className="font-mono">
{pool.reserve1.toLocaleString('en-US', { maximumFractionDigits: 0 })}
</p>
</div>
</div>
{/* User Position */}
{pool.userLpBalance && pool.userLpBalance > 0 && (
<div className="bg-green-500/10 border border-green-500/30 rounded-lg p-2 mb-3 text-sm">
<div className="flex justify-between">
<span className="text-green-400">{t('pools.yourPosition')}</span>
<span className="text-green-400 font-mono">
{pool.userShare?.toFixed(2)}%
</span>
</div>
<div className="text-xs text-muted-foreground mt-1">
LP Token: {pool.userLpBalance.toFixed(4)}
</div>
</div>
)}
{/* Action Buttons */}
<div className="flex gap-2">
<button
onClick={() => {
hapticImpact('light');
setSelectedPool(pool);
setIsAddingLiquidity(true);
}}
className="flex-1 py-2 bg-gradient-to-r from-green-600/20 to-blue-600/20 border border-green-500/30 text-green-400 font-medium rounded-lg flex items-center justify-center gap-1 text-sm"
>
<Plus className="w-4 h-4" />
{t('pools.addButton')}
</button>
{pool.userLpBalance && pool.userLpBalance > 0 && (
<button
onClick={() => {
hapticImpact('light');
setSelectedPool(pool);
setIsRemovingLiquidity(true);
}}
className="flex-1 py-2 bg-gradient-to-r from-red-600/20 to-orange-600/20 border border-red-500/30 text-red-400 font-medium rounded-lg flex items-center justify-center gap-1 text-sm"
>
<Minus className="w-4 h-4" />
{t('pools.removeButton')}
</button>
)}
</div>
</div>
))
)}
</div>
</div>
</div>
);
}