fix: Enable dynamic liquidity pools and improve USDT display consistency

- Make AddLiquidityModal dynamic to support all pool combinations (HEZ-PEZ, HEZ-USDT, PEZ-USDT)
- Pass asset0 and asset1 props from PoolDashboard to AddLiquidityModal
- Rename balance key from wUSDT to USDT in WalletContext for user-facing consistency
- Display USDT instead of wUSDT in PriceChart for better UX
- Backend still uses wUSDT (Asset ID 2), but frontend shows USDT to users

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-06 19:55:57 +03:00
parent 7bbe83f089
commit b1850e2be8
4 changed files with 139 additions and 82 deletions
+126 -78
View File
@@ -5,33 +5,68 @@ import { usePolkadot } from '@/contexts/PolkadotContext';
import { useWallet } from '@/contexts/WalletContext'; import { useWallet } from '@/contexts/WalletContext';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Alert, AlertDescription } from '@/components/ui/alert'; import { Alert, AlertDescription } from '@/components/ui/alert';
import { ASSET_IDS, getAssetSymbol } from '@/lib/wallet';
interface AddLiquidityModalProps { interface AddLiquidityModalProps {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
asset0?: number; // Pool's first asset ID
asset1?: number; // Pool's second asset ID
} }
export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({ isOpen, onClose }) => { // 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 === 2) 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 === 2) return 'USDT';
return getAssetSymbol(assetId);
};
// Helper to get decimals for asset
const getAssetDecimals = (assetId: number): number => {
if (assetId === ASSET_IDS.WUSDT || assetId === 2) return 6; // wUSDT has 6 decimals
return 12; // wHEZ, PEZ have 12 decimals
};
export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
isOpen,
onClose,
asset0 = 0, // Default to wHEZ
asset1 = 1 // Default to PEZ
}) => {
const { api, selectedAccount, isApiReady } = usePolkadot(); const { api, selectedAccount, isApiReady } = usePolkadot();
const { balances, refreshBalances } = useWallet(); const { balances, refreshBalances } = useWallet();
const [whezAmount, setWhezAmount] = useState(''); const [amount0, setAmount0] = useState('');
const [pezAmount, setPezAmount] = useState(''); const [amount1, setAmount1] = useState('');
const [currentPrice, setCurrentPrice] = useState<number | null>(null); const [currentPrice, setCurrentPrice] = useState<number | null>(null);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false); 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);
// Fetch current pool price // Fetch current pool price
useEffect(() => { useEffect(() => {
if (!api || !isApiReady || !isOpen) return; if (!api || !isApiReady || !isOpen) return;
const fetchPoolPrice = async () => { const fetchPoolPrice = async () => {
try { try {
const asset1 = 0; // wHEZ const poolId = [asset0, asset1];
const asset2 = 1; // PEZ
const poolId = [asset1, asset2];
const poolInfo = await api.query.assetConversion.pools(poolId); const poolInfo = await api.query.assetConversion.pools(poolId);
if (poolInfo.isSome) { if (poolInfo.isSome) {
@@ -39,30 +74,24 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({ isOpen, on
const { stringToU8a } = await import('@polkadot/util'); const { stringToU8a } = await import('@polkadot/util');
const { blake2AsU8a } = await import('@polkadot/util-crypto'); const { blake2AsU8a } = await import('@polkadot/util-crypto');
// PalletId for AssetConversion: "py/ascon" (8 bytes)
const PALLET_ID = stringToU8a('py/ascon'); const PALLET_ID = stringToU8a('py/ascon');
const poolIdType = api.createType('(u32, u32)', [asset0, asset1]);
// Create PoolId tuple (u32, u32)
const poolIdType = api.createType('(u32, u32)', [asset1, asset2]);
// Create (PalletId, PoolId) tuple: ([u8; 8], (u32, u32))
const palletIdType = api.createType('[u8; 8]', PALLET_ID); const palletIdType = api.createType('[u8; 8]', PALLET_ID);
const fullTuple = api.createType('([u8; 8], (u32, u32))', [palletIdType, poolIdType]); const fullTuple = api.createType('([u8; 8], (u32, u32))', [palletIdType, poolIdType]);
// Hash the SCALE-encoded tuple
const accountHash = blake2AsU8a(fullTuple.toU8a(), 256); const accountHash = blake2AsU8a(fullTuple.toU8a(), 256);
const poolAccountId = api.createType('AccountId32', accountHash); const poolAccountId = api.createType('AccountId32', accountHash);
// Get reserves // Get reserves
const whezBalanceData = await api.query.assets.account(asset1, poolAccountId); const balance0Data = await api.query.assets.account(asset0, poolAccountId);
const pezBalanceData = await api.query.assets.account(asset2, poolAccountId); const balance1Data = await api.query.assets.account(asset1, poolAccountId);
if (whezBalanceData.isSome && pezBalanceData.isSome) { if (balance0Data.isSome && balance1Data.isSome) {
const whezData = whezBalanceData.unwrap().toJSON() as any; const data0 = balance0Data.unwrap().toJSON() as any;
const pezData = pezBalanceData.unwrap().toJSON() as any; const data1 = balance1Data.unwrap().toJSON() as any;
const reserve0 = Number(whezData.balance) / 1e12; const reserve0 = Number(data0.balance) / Math.pow(10, asset0Decimals);
const reserve1 = Number(pezData.balance) / 1e12; const reserve1 = Number(data1.balance) / Math.pow(10, asset1Decimals);
setCurrentPrice(reserve1 / reserve0); setCurrentPrice(reserve1 / reserve0);
} }
@@ -73,40 +102,43 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({ isOpen, on
}; };
fetchPoolPrice(); fetchPoolPrice();
}, [api, isApiReady, isOpen]); }, [api, isApiReady, isOpen, asset0, asset1, asset0Decimals, asset1Decimals]);
// Auto-calculate PEZ amount based on wHEZ input // Auto-calculate asset1 amount based on asset0 input
useEffect(() => { useEffect(() => {
if (whezAmount && currentPrice) { if (amount0 && currentPrice) {
const calculatedPez = parseFloat(whezAmount) * currentPrice; const calculated = parseFloat(amount0) * currentPrice;
setPezAmount(calculatedPez.toFixed(4)); setAmount1(calculated.toFixed(asset1Decimals === 6 ? 2 : 4));
} else if (!whezAmount) { } else if (!amount0) {
setPezAmount(''); setAmount1('');
} }
}, [whezAmount, currentPrice]); }, [amount0, currentPrice, asset1Decimals]);
const handleAddLiquidity = async () => { const handleAddLiquidity = async () => {
if (!api || !selectedAccount || !whezAmount || !pezAmount) return; if (!api || !selectedAccount || !amount0 || !amount1) return;
setIsLoading(true); setIsLoading(true);
setError(null); setError(null);
try { try {
// Validate amounts // Validate amounts
if (parseFloat(whezAmount) <= 0 || parseFloat(pezAmount) <= 0) { if (parseFloat(amount0) <= 0 || parseFloat(amount1) <= 0) {
setError('Please enter valid amounts'); setError('Please enter valid amounts');
setIsLoading(false); setIsLoading(false);
return; return;
} }
if (parseFloat(whezAmount) > whezBalance) { const balance0 = (balances as any)[asset0BalanceKey] || 0;
setError('Insufficient HEZ balance'); const balance1 = (balances as any)[asset1BalanceKey] || 0;
if (parseFloat(amount0) > balance0) {
setError(`Insufficient ${asset0Name} balance`);
setIsLoading(false); setIsLoading(false);
return; return;
} }
if (parseFloat(pezAmount) > pezBalance) { if (parseFloat(amount1) > balance1) {
setError('Insufficient PEZ balance'); setError(`Insufficient ${asset1Name} balance`);
setIsLoading(false); setIsLoading(false);
return; return;
} }
@@ -114,29 +146,45 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({ isOpen, on
// Get the signer from the extension // Get the signer from the extension
const injector = await web3FromAddress(selectedAccount.address); const injector = await web3FromAddress(selectedAccount.address);
const whezAmountBN = BigInt(Math.floor(parseFloat(whezAmount) * 1e12)); // Convert amounts to proper decimals
const pezAmountBN = BigInt(Math.floor(parseFloat(pezAmount) * 1e12)); 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 - more tolerance for AMM) // Min amounts (90% of desired to account for slippage)
const minWhezBN = (whezAmountBN * BigInt(90)) / BigInt(100); const minAmount0BN = (amount0BN * BigInt(90)) / BigInt(100);
const minPezBN = (pezAmountBN * BigInt(90)) / BigInt(100); const minAmount1BN = (amount1BN * BigInt(90)) / BigInt(100);
// Need to wrap HEZ to wHEZ first // Build transaction(s)
const wrapTx = api.tx.tokenWrapper.wrap(whezAmountBN.toString()); let tx;
// Add liquidity transaction // If asset0 is HEZ (0), need to wrap it first
const addLiquidityTx = api.tx.assetConversion.addLiquidity( if (asset0 === 0 || asset0 === ASSET_IDS.WHEZ) {
0, // asset1 (wHEZ) const wrapTx = api.tx.tokenWrapper.wrap(amount0BN.toString());
1, // asset2 (PEZ)
whezAmountBN.toString(),
pezAmountBN.toString(),
minWhezBN.toString(),
minPezBN.toString(),
selectedAccount.address
);
// Batch transactions const addLiquidityTx = api.tx.assetConversion.addLiquidity(
const tx = api.tx.utility.batchAll([wrapTx, addLiquidityTx]); 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( await tx.signAndSend(
selectedAccount.address, selectedAccount.address,
@@ -167,7 +215,6 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({ isOpen, on
} }
} }
// Also check events for more details
events.forEach(({ event }) => { events.forEach(({ event }) => {
if (api.events.system.ExtrinsicFailed.is(event)) { if (api.events.system.ExtrinsicFailed.is(event)) {
console.error('ExtrinsicFailed event:', event.toHuman()); console.error('ExtrinsicFailed event:', event.toHuman());
@@ -180,8 +227,8 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({ isOpen, on
console.log('Transaction successful'); console.log('Transaction successful');
setSuccess(true); setSuccess(true);
setIsLoading(false); setIsLoading(false);
setWhezAmount(''); setAmount0('');
setPezAmount(''); setAmount1('');
refreshBalances(); refreshBalances();
setTimeout(() => { setTimeout(() => {
@@ -201,8 +248,8 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({ isOpen, on
if (!isOpen) return null; if (!isOpen) return null;
const whezBalance = balances.HEZ || 0; const balance0 = (balances as any)[asset0BalanceKey] || 0;
const pezBalance = balances.PEZ || 0; const balance1 = (balances as any)[asset1BalanceKey] || 0;
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
@@ -233,33 +280,34 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({ isOpen, on
<Alert className="mb-4 bg-blue-900/20 border-blue-500"> <Alert className="mb-4 bg-blue-900/20 border-blue-500">
<Info className="h-4 w-4" /> <Info className="h-4 w-4" />
<AlertDescription className="text-sm"> <AlertDescription className="text-sm">
Add liquidity to earn 3% fees from all swaps. Your HEZ will be automatically wrapped to wHEZ. Add liquidity to earn 3% fees from all swaps.
{(asset0 === 0 || asset0 === ASSET_IDS.WHEZ) && ' Your HEZ will be automatically wrapped to wHEZ.'}
</AlertDescription> </AlertDescription>
</Alert> </Alert>
<div className="space-y-4"> <div className="space-y-4">
{/* HEZ Input */} {/* Asset 0 Input */}
<div> <div>
<label className="block text-sm font-medium text-gray-300 mb-2"> <label className="block text-sm font-medium text-gray-300 mb-2">
HEZ Amount {asset0Name} Amount
</label> </label>
<div className="relative"> <div className="relative">
<input <input
type="number" type="number"
value={whezAmount} value={amount0}
onChange={(e) => setWhezAmount(e.target.value)} onChange={(e) => setAmount0(e.target.value)}
placeholder="0.0" placeholder="0.0"
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" 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} disabled={isLoading}
/> />
<div className="absolute right-3 top-3 flex items-center gap-2"> <div className="absolute right-3 top-3 flex items-center gap-2">
<span className="text-gray-400 text-sm">HEZ</span> <span className="text-gray-400 text-sm">{asset0Name}</span>
</div> </div>
</div> </div>
<div className="flex justify-between mt-1 text-xs text-gray-400"> <div className="flex justify-between mt-1 text-xs text-gray-400">
<span>Balance: {whezBalance.toLocaleString()}</span> <span>Balance: {balance0.toLocaleString()}</span>
<button <button
onClick={() => setWhezAmount(whezBalance.toString())} onClick={() => setAmount0(balance0.toString())}
className="text-blue-400 hover:text-blue-300" className="text-blue-400 hover:text-blue-300"
> >
Max Max
@@ -271,34 +319,34 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({ isOpen, on
<Plus className="w-5 h-5 text-gray-400" /> <Plus className="w-5 h-5 text-gray-400" />
</div> </div>
{/* PEZ Input */} {/* Asset 1 Input */}
<div> <div>
<label className="block text-sm font-medium text-gray-300 mb-2"> <label className="block text-sm font-medium text-gray-300 mb-2">
PEZ Amount (Auto-calculated) {asset1Name} Amount (Auto-calculated)
</label> </label>
<div className="relative"> <div className="relative">
<input <input
type="number" type="number"
value={pezAmount} value={amount1}
placeholder="0.0" placeholder="0.0"
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-gray-400 focus:outline-none cursor-not-allowed" className="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-gray-400 focus:outline-none cursor-not-allowed"
disabled={true} disabled={true}
readOnly readOnly
/> />
<div className="absolute right-3 top-3 flex items-center gap-2"> <div className="absolute right-3 top-3 flex items-center gap-2">
<span className="text-gray-400 text-sm">PEZ</span> <span className="text-gray-400 text-sm">{asset1Name}</span>
</div> </div>
</div> </div>
<div className="flex justify-between mt-1 text-xs text-gray-400"> <div className="flex justify-between mt-1 text-xs text-gray-400">
<span>Balance: {pezBalance.toLocaleString()}</span> <span>Balance: {balance1.toLocaleString()}</span>
<span> <span>
{currentPrice && `Rate: 1 HEZ = ${currentPrice.toFixed(4)} PEZ`} {currentPrice && `Rate: 1 ${asset0Name} = ${currentPrice.toFixed(asset1Decimals === 6 ? 2 : 4)} ${asset1Name}`}
</span> </span>
</div> </div>
</div> </div>
{/* Price Info */} {/* Price Info */}
{whezAmount && pezAmount && ( {amount0 && amount1 && (
<div className="bg-gray-800 rounded-lg p-3 space-y-2 text-sm"> <div className="bg-gray-800 rounded-lg p-3 space-y-2 text-sm">
<div className="flex justify-between text-gray-300"> <div className="flex justify-between text-gray-300">
<span>Share of Pool</span> <span>Share of Pool</span>
@@ -315,10 +363,10 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({ isOpen, on
onClick={handleAddLiquidity} onClick={handleAddLiquidity}
disabled={ disabled={
isLoading || isLoading ||
!whezAmount || !amount0 ||
!pezAmount || !amount1 ||
parseFloat(whezAmount) > whezBalance || parseFloat(amount0) > balance0 ||
parseFloat(pezAmount) > pezBalance parseFloat(amount1) > balance1
} }
className="w-full bg-gradient-to-r from-green-600 to-blue-600 hover:from-green-700 hover:to-blue-700 h-12" className="w-full bg-gradient-to-r from-green-600 to-blue-600 hover:from-green-700 hover:to-blue-700 h-12"
> >
+2
View File
@@ -580,6 +580,8 @@ const PoolDashboard = () => {
<AddLiquidityModal <AddLiquidityModal
isOpen={isAddLiquidityModalOpen} isOpen={isAddLiquidityModalOpen}
onClose={() => setIsAddLiquidityModalOpen(false)} onClose={() => setIsAddLiquidityModalOpen(false)}
asset0={poolData?.asset0}
asset1={poolData?.asset1}
/> />
{lpPosition && poolData && ( {lpPosition && poolData && (
+8 -1
View File
@@ -10,6 +10,13 @@ interface PriceChartProps {
currentPrice: number; currentPrice: number;
} }
// Helper: Convert backend token symbols to user-facing display names
const getDisplayName = (token: string): string => {
if (token === 'wUSDT') return 'USDT';
if (token === 'wHEZ') return 'HEZ';
return token; // HEZ, PEZ, etc. remain the same
};
export const PriceChart: React.FC<PriceChartProps> = ({ fromToken, toToken, currentPrice }) => { export const PriceChart: React.FC<PriceChartProps> = ({ fromToken, toToken, currentPrice }) => {
const [timeframe, setTimeframe] = useState<'1H' | '24H' | '7D' | '30D'>('24H'); const [timeframe, setTimeframe] = useState<'1H' | '24H' | '7D' | '30D'>('24H');
const [chartData, setChartData] = useState<any[]>([]); const [chartData, setChartData] = useState<any[]>([]);
@@ -83,7 +90,7 @@ export const PriceChart: React.FC<PriceChartProps> = ({ fromToken, toToken, curr
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<div> <div>
<div className="text-sm text-gray-400 mb-1"> <div className="text-sm text-gray-400 mb-1">
{fromToken}/{toToken} Price {getDisplayName(fromToken)}/{getDisplayName(toToken)} Price
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="text-2xl font-bold text-white"> <span className="text-2xl font-bold text-white">
+3 -3
View File
@@ -13,7 +13,7 @@ interface TokenBalances {
HEZ: string; HEZ: string;
PEZ: string; PEZ: string;
wHEZ: string; wHEZ: string;
wUSDT: string; USDT: string; // User-facing key for wUSDT (backend uses wUSDT asset ID 2)
} }
interface WalletContextType { interface WalletContextType {
@@ -44,7 +44,7 @@ export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ childr
}); });
const [balance, setBalance] = useState<string>('0'); const [balance, setBalance] = useState<string>('0');
const [balances, setBalances] = useState<TokenBalances>({ HEZ: '0', PEZ: '0', wHEZ: '0', wUSDT: '0' }); const [balances, setBalances] = useState<TokenBalances>({ HEZ: '0', PEZ: '0', wHEZ: '0', USDT: '0' });
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
// Fetch all token balances when account changes // Fetch all token balances when account changes
@@ -120,7 +120,7 @@ export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ childr
HEZ: hezBalance, HEZ: hezBalance,
PEZ: pezBalance, PEZ: pezBalance,
wHEZ: whezBalance, wHEZ: whezBalance,
wUSDT: wusdtBalance, USDT: wusdtBalance,
}); });
console.log('✅ Balances updated:', { HEZ: hezBalance, PEZ: pezBalance, wHEZ: whezBalance, wUSDT: wusdtBalance }); console.log('✅ Balances updated:', { HEZ: hezBalance, PEZ: pezBalance, wHEZ: whezBalance, wUSDT: wusdtBalance });