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 5686386ef6
commit 617b61030a
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 { Button } from '@/components/ui/button';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { ASSET_IDS, getAssetSymbol } from '@/lib/wallet';
interface AddLiquidityModalProps {
isOpen: boolean;
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 { balances, refreshBalances } = useWallet();
const [whezAmount, setWhezAmount] = useState('');
const [pezAmount, setPezAmount] = useState('');
const [amount0, setAmount0] = useState('');
const [amount1, setAmount1] = useState('');
const [currentPrice, setCurrentPrice] = useState<number | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(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);
// Fetch current pool price
useEffect(() => {
if (!api || !isApiReady || !isOpen) return;
const fetchPoolPrice = async () => {
try {
const asset1 = 0; // wHEZ
const asset2 = 1; // PEZ
const poolId = [asset1, asset2];
const poolId = [asset0, asset1];
const poolInfo = await api.query.assetConversion.pools(poolId);
if (poolInfo.isSome) {
@@ -39,30 +74,24 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({ isOpen, on
const { stringToU8a } = await import('@polkadot/util');
const { blake2AsU8a } = await import('@polkadot/util-crypto');
// PalletId for AssetConversion: "py/ascon" (8 bytes)
const PALLET_ID = stringToU8a('py/ascon');
// Create PoolId tuple (u32, u32)
const poolIdType = api.createType('(u32, u32)', [asset1, asset2]);
// Create (PalletId, PoolId) tuple: ([u8; 8], (u32, u32))
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]);
// Hash the SCALE-encoded tuple
const accountHash = blake2AsU8a(fullTuple.toU8a(), 256);
const poolAccountId = api.createType('AccountId32', accountHash);
// Get reserves
const whezBalanceData = await api.query.assets.account(asset1, poolAccountId);
const pezBalanceData = await api.query.assets.account(asset2, poolAccountId);
const balance0Data = await api.query.assets.account(asset0, poolAccountId);
const balance1Data = await api.query.assets.account(asset1, poolAccountId);
if (whezBalanceData.isSome && pezBalanceData.isSome) {
const whezData = whezBalanceData.unwrap().toJSON() as any;
const pezData = pezBalanceData.unwrap().toJSON() as any;
if (balance0Data.isSome && balance1Data.isSome) {
const data0 = balance0Data.unwrap().toJSON() as any;
const data1 = balance1Data.unwrap().toJSON() as any;
const reserve0 = Number(whezData.balance) / 1e12;
const reserve1 = Number(pezData.balance) / 1e12;
const reserve0 = Number(data0.balance) / Math.pow(10, asset0Decimals);
const reserve1 = Number(data1.balance) / Math.pow(10, asset1Decimals);
setCurrentPrice(reserve1 / reserve0);
}
@@ -73,40 +102,43 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({ isOpen, on
};
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(() => {
if (whezAmount && currentPrice) {
const calculatedPez = parseFloat(whezAmount) * currentPrice;
setPezAmount(calculatedPez.toFixed(4));
} else if (!whezAmount) {
setPezAmount('');
if (amount0 && currentPrice) {
const calculated = parseFloat(amount0) * currentPrice;
setAmount1(calculated.toFixed(asset1Decimals === 6 ? 2 : 4));
} else if (!amount0) {
setAmount1('');
}
}, [whezAmount, currentPrice]);
}, [amount0, currentPrice, asset1Decimals]);
const handleAddLiquidity = async () => {
if (!api || !selectedAccount || !whezAmount || !pezAmount) return;
if (!api || !selectedAccount || !amount0 || !amount1) return;
setIsLoading(true);
setError(null);
try {
// Validate amounts
if (parseFloat(whezAmount) <= 0 || parseFloat(pezAmount) <= 0) {
if (parseFloat(amount0) <= 0 || parseFloat(amount1) <= 0) {
setError('Please enter valid amounts');
setIsLoading(false);
return;
}
if (parseFloat(whezAmount) > whezBalance) {
setError('Insufficient HEZ balance');
const balance0 = (balances as any)[asset0BalanceKey] || 0;
const balance1 = (balances as any)[asset1BalanceKey] || 0;
if (parseFloat(amount0) > balance0) {
setError(`Insufficient ${asset0Name} balance`);
setIsLoading(false);
return;
}
if (parseFloat(pezAmount) > pezBalance) {
setError('Insufficient PEZ balance');
if (parseFloat(amount1) > balance1) {
setError(`Insufficient ${asset1Name} balance`);
setIsLoading(false);
return;
}
@@ -114,29 +146,45 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({ isOpen, on
// Get the signer from the extension
const injector = await web3FromAddress(selectedAccount.address);
const whezAmountBN = BigInt(Math.floor(parseFloat(whezAmount) * 1e12));
const pezAmountBN = BigInt(Math.floor(parseFloat(pezAmount) * 1e12));
// 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 - more tolerance for AMM)
const minWhezBN = (whezAmountBN * BigInt(90)) / BigInt(100);
const minPezBN = (pezAmountBN * BigInt(90)) / BigInt(100);
// Min amounts (90% of desired to account for slippage)
const minAmount0BN = (amount0BN * BigInt(90)) / BigInt(100);
const minAmount1BN = (amount1BN * BigInt(90)) / BigInt(100);
// Need to wrap HEZ to wHEZ first
const wrapTx = api.tx.tokenWrapper.wrap(whezAmountBN.toString());
// Build transaction(s)
let tx;
// Add liquidity transaction
const addLiquidityTx = api.tx.assetConversion.addLiquidity(
0, // asset1 (wHEZ)
1, // asset2 (PEZ)
whezAmountBN.toString(),
pezAmountBN.toString(),
minWhezBN.toString(),
minPezBN.toString(),
selectedAccount.address
);
// 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());
// Batch transactions
const tx = api.tx.utility.batchAll([wrapTx, addLiquidityTx]);
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,
@@ -167,7 +215,6 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({ isOpen, on
}
}
// Also check events for more details
events.forEach(({ event }) => {
if (api.events.system.ExtrinsicFailed.is(event)) {
console.error('ExtrinsicFailed event:', event.toHuman());
@@ -180,8 +227,8 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({ isOpen, on
console.log('Transaction successful');
setSuccess(true);
setIsLoading(false);
setWhezAmount('');
setPezAmount('');
setAmount0('');
setAmount1('');
refreshBalances();
setTimeout(() => {
@@ -201,8 +248,8 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({ isOpen, on
if (!isOpen) return null;
const whezBalance = balances.HEZ || 0;
const pezBalance = balances.PEZ || 0;
const balance0 = (balances as any)[asset0BalanceKey] || 0;
const balance1 = (balances as any)[asset1BalanceKey] || 0;
return (
<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">
<Info className="h-4 w-4" />
<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>
</Alert>
<div className="space-y-4">
{/* HEZ Input */}
{/* Asset 0 Input */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
HEZ Amount
{asset0Name} Amount
</label>
<div className="relative">
<input
type="number"
value={whezAmount}
onChange={(e) => setWhezAmount(e.target.value)}
value={amount0}
onChange={(e) => setAmount0(e.target.value)}
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"
disabled={isLoading}
/>
<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 className="flex justify-between mt-1 text-xs text-gray-400">
<span>Balance: {whezBalance.toLocaleString()}</span>
<span>Balance: {balance0.toLocaleString()}</span>
<button
onClick={() => setWhezAmount(whezBalance.toString())}
onClick={() => setAmount0(balance0.toString())}
className="text-blue-400 hover:text-blue-300"
>
Max
@@ -271,34 +319,34 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({ isOpen, on
<Plus className="w-5 h-5 text-gray-400" />
</div>
{/* PEZ Input */}
{/* Asset 1 Input */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
PEZ Amount (Auto-calculated)
{asset1Name} Amount (Auto-calculated)
</label>
<div className="relative">
<input
type="number"
value={pezAmount}
value={amount1}
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"
disabled={true}
readOnly
/>
<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 className="flex justify-between mt-1 text-xs text-gray-400">
<span>Balance: {pezBalance.toLocaleString()}</span>
<span>Balance: {balance1.toLocaleString()}</span>
<span>
{currentPrice && `Rate: 1 HEZ = ${currentPrice.toFixed(4)} PEZ`}
{currentPrice && `Rate: 1 ${asset0Name} = ${currentPrice.toFixed(asset1Decimals === 6 ? 2 : 4)} ${asset1Name}`}
</span>
</div>
</div>
{/* Price Info */}
{whezAmount && pezAmount && (
{amount0 && amount1 && (
<div className="bg-gray-800 rounded-lg p-3 space-y-2 text-sm">
<div className="flex justify-between text-gray-300">
<span>Share of Pool</span>
@@ -315,10 +363,10 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({ isOpen, on
onClick={handleAddLiquidity}
disabled={
isLoading ||
!whezAmount ||
!pezAmount ||
parseFloat(whezAmount) > whezBalance ||
parseFloat(pezAmount) > pezBalance
!amount0 ||
!amount1 ||
parseFloat(amount0) > balance0 ||
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"
>
+2
View File
@@ -580,6 +580,8 @@ const PoolDashboard = () => {
<AddLiquidityModal
isOpen={isAddLiquidityModalOpen}
onClose={() => setIsAddLiquidityModalOpen(false)}
asset0={poolData?.asset0}
asset1={poolData?.asset1}
/>
{lpPosition && poolData && (
+8 -1
View File
@@ -10,6 +10,13 @@ interface PriceChartProps {
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 }) => {
const [timeframe, setTimeframe] = useState<'1H' | '24H' | '7D' | '30D'>('24H');
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>
<div className="text-sm text-gray-400 mb-1">
{fromToken}/{toToken} Price
{getDisplayName(fromToken)}/{getDisplayName(toToken)} Price
</div>
<div className="flex items-center gap-3">
<span className="text-2xl font-bold text-white">
+3 -3
View File
@@ -13,7 +13,7 @@ interface TokenBalances {
HEZ: string;
PEZ: string;
wHEZ: string;
wUSDT: string;
USDT: string; // User-facing key for wUSDT (backend uses wUSDT asset ID 2)
}
interface WalletContextType {
@@ -44,7 +44,7 @@ export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ childr
});
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);
// Fetch all token balances when account changes
@@ -120,7 +120,7 @@ export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ childr
HEZ: hezBalance,
PEZ: pezBalance,
wHEZ: whezBalance,
wUSDT: wusdtBalance,
USDT: wusdtBalance,
});
console.log('✅ Balances updated:', { HEZ: hezBalance, PEZ: pezBalance, wHEZ: whezBalance, wUSDT: wusdtBalance });