feat: Add USDT support, dynamic pricing, and balance validation

Major improvements to wallet dashboard and DEX functionality:

## USDT Integration
- Add wUSDT (Asset ID 2) support with 6 decimal precision
- Display USDT balances in wallet dashboard
- Integrate USDT into swap and pool interfaces

## Dynamic Token Pricing
- Fetch real-time HEZ and PEZ prices from liquidity pools
- Calculate USD values using pool reserve ratios
- Display live USD equivalent for token balances

## User Experience Improvements
- Hide wrapped tokens (wHEZ, wUSDT) from user interface
- Show only HEZ, PEZ, USDT as user-facing tokens
- Handle wrapping/unwrapping transparently in backend
- Add balance validation before swap transactions
- Prevent insufficient balance swaps with clear warnings

## Pool Dashboard Enhancements
- Support multiple pool pairs (HEZ/PEZ, HEZ/USDT, PEZ/USDT)
- Dynamic pool selection interface
- User-friendly token names throughout pool interface

## Technical Improvements
- Correct decimal handling (6 for USDT, 12 for others)
- Proper pool account ID derivation using blake2 hash
- Balance subscriptions for real-time updates
- Custom token support with Add Token modal

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-05 10:56:44 +03:00
parent 262a0da939
commit 4825ac3839
6 changed files with 822 additions and 95 deletions
+96 -16
View File
@@ -76,6 +76,13 @@ const TokenSwap = () => {
return token?.displaySymbol || tokenSymbol;
};
// Check if user has insufficient balance
const hasInsufficientBalance = React.useMemo(() => {
const fromAmountNum = parseFloat(fromAmount || '0');
const fromBalanceNum = parseFloat(fromBalance?.toString() || '0');
return fromAmountNum > 0 && fromAmountNum > fromBalanceNum;
}, [fromAmount, fromBalance]);
// Calculate toAmount and price impact using AMM constant product formula
const swapCalculations = React.useMemo(() => {
if (!fromAmount || !poolReserves || parseFloat(fromAmount) <= 0) {
@@ -489,15 +496,48 @@ const TokenSwap = () => {
return;
}
// ✅ BALANCE VALIDATION - Check if user has sufficient balance
const fromAmountNum = parseFloat(fromAmount);
const fromBalanceNum = parseFloat(fromBalance?.toString() || '0');
if (fromAmountNum > fromBalanceNum) {
toast({
title: 'Insufficient Balance',
description: `You only have ${fromBalanceNum.toFixed(4)} ${getTokenDisplayName(fromToken)}. Cannot swap ${fromAmountNum} ${getTokenDisplayName(fromToken)}.`,
variant: 'destructive',
});
return;
}
setIsSwapping(true);
setShowConfirm(false); // Close dialog before transaction starts
try {
const amountIn = parseAmount(fromAmount, 12);
// Get correct decimals for each token
const getTokenDecimals = (token: string) => {
if (token === 'wUSDT') return 6; // wUSDT has 6 decimals
return 12; // HEZ, wHEZ, PEZ all have 12 decimals
};
const fromDecimals = getTokenDecimals(fromToken);
const toDecimals = getTokenDecimals(toToken);
const amountIn = parseAmount(fromAmount, fromDecimals);
const minAmountOut = parseAmount(
(parseFloat(toAmount) * (1 - parseFloat(slippage) / 100)).toString(),
12
toDecimals
);
console.log('💰 Swap amounts:', {
fromToken,
toToken,
fromAmount,
toAmount,
fromDecimals,
toDecimals,
amountIn: amountIn.toString(),
minAmountOut: minAmountOut.toString()
});
// Get signer from extension
const { web3FromAddress } = await import('@polkadot/extension-dapp');
const injector = await web3FromAddress(selectedAccount.address);
@@ -533,18 +573,42 @@ const TokenSwap = () => {
const unwrapTx = api.tx.tokenWrapper.unwrap(minAmountOut.toString());
tx = api.tx.utility.batchAll([swapTx, unwrapTx]);
} else {
// Direct swap between assets (should not happen with HEZ/PEZ only)
const getPoolAssetId = (token: string) => {
if (token === 'HEZ') return 0; // wHEZ
return ASSET_IDS[token as keyof typeof ASSET_IDS];
};
} else if (fromToken === 'HEZ') {
// HEZ → Any Asset: wrap(HEZ→wHEZ) then swap(wHEZ→Asset)
const wrapTx = api.tx.tokenWrapper.wrap(amountIn.toString());
// Map token symbol to asset ID
const toAssetId = toToken === 'PEZ' ? 1 : toToken === 'wUSDT' ? 2 : ASSET_IDS[toToken as keyof typeof ASSET_IDS];
const swapPath = [0, toAssetId]; // wHEZ → target asset
const swapTx = api.tx.assetConversion.swapExactTokensForTokens(
swapPath,
amountIn.toString(),
minAmountOut.toString(),
selectedAccount.address,
true
);
tx = api.tx.utility.batchAll([wrapTx, swapTx]);
// AssetKind = u32, so swap path is just [assetId1, assetId2]
const swapPath = [
getPoolAssetId(fromToken),
getPoolAssetId(toToken)
];
} else if (toToken === 'HEZ') {
// Any Asset → HEZ: swap(Asset→wHEZ) then unwrap(wHEZ→HEZ)
// Map token symbol to asset ID
const fromAssetId = fromToken === 'PEZ' ? 1 : fromToken === 'wUSDT' ? 2 : ASSET_IDS[fromToken as keyof typeof ASSET_IDS];
const swapPath = [fromAssetId, 0]; // source asset → wHEZ
const swapTx = api.tx.assetConversion.swapExactTokensForTokens(
swapPath,
amountIn.toString(),
minAmountOut.toString(),
selectedAccount.address,
true
);
const unwrapTx = api.tx.tokenWrapper.unwrap(minAmountOut.toString());
tx = api.tx.utility.batchAll([swapTx, unwrapTx]);
} else {
// Direct swap between assets (PEZ ↔ wUSDT, etc.)
// Map token symbols to asset IDs
const fromAssetId = fromToken === 'PEZ' ? 1 : fromToken === 'wUSDT' ? 2 : ASSET_IDS[fromToken as keyof typeof ASSET_IDS];
const toAssetId = toToken === 'PEZ' ? 1 : toToken === 'wUSDT' ? 2 : ASSET_IDS[toToken as keyof typeof ASSET_IDS];
const swapPath = [fromAssetId, toAssetId];
tx = api.tx.assetConversion.swapExactTokensForTokens(
swapPath,
@@ -978,8 +1042,18 @@ const TokenSwap = () => {
</div>
</div>
{/* Insufficient Balance Warning */}
{hasInsufficientBalance && (
<Alert className="bg-red-900/20 border-red-500/30">
<AlertTriangle className="h-4 w-4 text-red-500" />
<AlertDescription className="text-red-300 text-sm">
Insufficient {getTokenDisplayName(fromToken)} balance. You have {fromBalance} {getTokenDisplayName(fromToken)} but trying to swap {fromAmount} {getTokenDisplayName(fromToken)}.
</AlertDescription>
</Alert>
)}
{/* High Price Impact Warning (>5%) */}
{priceImpact >= 5 && (
{priceImpact >= 5 && !hasInsufficientBalance && (
<Alert className="bg-red-900/20 border-red-500/30">
<AlertTriangle className="h-4 w-4 text-red-500" />
<AlertDescription className="text-red-300 text-sm">
@@ -991,9 +1065,15 @@ const TokenSwap = () => {
<Button
className="w-full h-12 text-lg"
onClick={() => setShowConfirm(true)}
disabled={!fromAmount || parseFloat(fromAmount) <= 0 || !selectedAccount || exchangeRate === 0}
disabled={!fromAmount || parseFloat(fromAmount) <= 0 || !selectedAccount || exchangeRate === 0 || hasInsufficientBalance}
>
{!selectedAccount ? 'Connect Wallet' : exchangeRate === 0 ? 'No Pool Available' : 'Swap Tokens'}
{!selectedAccount
? 'Connect Wallet'
: hasInsufficientBalance
? `Insufficient ${getTokenDisplayName(fromToken)} Balance`
: exchangeRate === 0
? 'No Pool Available'
: 'Swap Tokens'}
</Button>
</div>
</Card>