mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 04:27:56 +00:00
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:
@@ -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"
|
||||
>
|
||||
|
||||
@@ -580,6 +580,8 @@ const PoolDashboard = () => {
|
||||
<AddLiquidityModal
|
||||
isOpen={isAddLiquidityModalOpen}
|
||||
onClose={() => setIsAddLiquidityModalOpen(false)}
|
||||
asset0={poolData?.asset0}
|
||||
asset1={poolData?.asset1}
|
||||
/>
|
||||
|
||||
{lpPosition && poolData && (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user