mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 04:27:56 +00:00
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:
@@ -1,8 +1,20 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Wallet, TrendingUp, ArrowUpRight, ArrowDownRight, RefreshCw, Award } from 'lucide-react';
|
||||
import { Wallet, TrendingUp, ArrowUpRight, ArrowDownRight, RefreshCw, Award, Plus, Coins, Send } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ASSET_IDS, getAssetSymbol } from '@/lib/wallet';
|
||||
import { AddTokenModal } from './AddTokenModal';
|
||||
import { TransferModal } from './TransferModal';
|
||||
|
||||
interface TokenBalance {
|
||||
assetId: number;
|
||||
symbol: string;
|
||||
name: string;
|
||||
balance: string;
|
||||
decimals: number;
|
||||
usdValue: number;
|
||||
}
|
||||
|
||||
export const AccountBalance: React.FC = () => {
|
||||
const { api, isApiReady, selectedAccount } = usePolkadot();
|
||||
@@ -16,8 +28,189 @@ export const AccountBalance: React.FC = () => {
|
||||
total: '0',
|
||||
});
|
||||
const [pezBalance, setPezBalance] = useState<string>('0');
|
||||
const [usdtBalance, setUsdtBalance] = useState<string>('0');
|
||||
const [hezUsdPrice, setHezUsdPrice] = useState<number>(0);
|
||||
const [pezUsdPrice, setPezUsdPrice] = useState<number>(0);
|
||||
const [trustScore, setTrustScore] = useState<string>('-');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [otherTokens, setOtherTokens] = useState<TokenBalance[]>([]);
|
||||
const [isAddTokenModalOpen, setIsAddTokenModalOpen] = useState(false);
|
||||
const [isTransferModalOpen, setIsTransferModalOpen] = useState(false);
|
||||
const [selectedTokenForTransfer, setSelectedTokenForTransfer] = useState<TokenBalance | null>(null);
|
||||
const [customTokenIds, setCustomTokenIds] = useState<number[]>(() => {
|
||||
const stored = localStorage.getItem('customTokenIds');
|
||||
return stored ? JSON.parse(stored) : [];
|
||||
});
|
||||
|
||||
// Helper function to get asset decimals
|
||||
const getAssetDecimals = (assetId: number): number => {
|
||||
if (assetId === ASSET_IDS.WUSDT) return 6; // wUSDT has 6 decimals
|
||||
return 12; // wHEZ, PEZ and others have 12 decimals by default
|
||||
};
|
||||
|
||||
// Helper to decode hex string to UTF-8
|
||||
const hexToString = (hex: string): string => {
|
||||
if (!hex || hex === '0x') return '';
|
||||
try {
|
||||
const hexStr = hex.startsWith('0x') ? hex.slice(2) : hex;
|
||||
const bytes = new Uint8Array(hexStr.match(/.{1,2}/g)?.map(byte => parseInt(byte, 16)) || []);
|
||||
return new TextDecoder('utf-8').decode(bytes).replace(/\0/g, '');
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
// Get token color based on assetId
|
||||
const getTokenColor = (assetId: number) => {
|
||||
const colors = {
|
||||
[ASSET_IDS.WHEZ]: { bg: 'from-green-500/20 to-yellow-500/20', text: 'text-green-400', border: 'border-green-500/30' },
|
||||
[ASSET_IDS.WUSDT]: { bg: 'from-emerald-500/20 to-teal-500/20', text: 'text-emerald-400', border: 'border-emerald-500/30' },
|
||||
};
|
||||
return colors[assetId] || { bg: 'from-cyan-500/20 to-blue-500/20', text: 'text-cyan-400', border: 'border-cyan-500/30' };
|
||||
};
|
||||
|
||||
// Fetch token prices from pools using pool account ID
|
||||
const fetchTokenPrices = async () => {
|
||||
if (!api || !isApiReady) return;
|
||||
|
||||
try {
|
||||
console.log('💰 Fetching token prices from pools...');
|
||||
|
||||
// Import utilities for pool account derivation
|
||||
const { stringToU8a } = await import('@polkadot/util');
|
||||
const { blake2AsU8a } = await import('@polkadot/util-crypto');
|
||||
const PALLET_ID = stringToU8a('py/ascon');
|
||||
|
||||
// Fetch wHEZ/wUSDT pool reserves (Asset 0 / Asset 2)
|
||||
const whezPoolId = api.createType('(u32, u32)', [0, 2]);
|
||||
const whezPalletIdType = api.createType('[u8; 8]', PALLET_ID);
|
||||
const whezFullTuple = api.createType('([u8; 8], (u32, u32))', [whezPalletIdType, whezPoolId]);
|
||||
const whezAccountHash = blake2AsU8a(whezFullTuple.toU8a(), 256);
|
||||
const whezPoolAccountId = api.createType('AccountId32', whezAccountHash);
|
||||
|
||||
const whezReserve0Query = await api.query.assets.account(0, whezPoolAccountId);
|
||||
const whezReserve1Query = await api.query.assets.account(2, whezPoolAccountId);
|
||||
|
||||
if (whezReserve0Query.isSome && whezReserve1Query.isSome) {
|
||||
const reserve0Data = whezReserve0Query.unwrap();
|
||||
const reserve1Data = whezReserve1Query.unwrap();
|
||||
|
||||
const reserve0 = BigInt(reserve0Data.balance.toString()); // wHEZ (12 decimals)
|
||||
const reserve1 = BigInt(reserve1Data.balance.toString()); // wUSDT (6 decimals)
|
||||
|
||||
// Calculate price: 1 HEZ = ? USD
|
||||
const hezPrice = Number(reserve1 * BigInt(10 ** 12)) / Number(reserve0 * BigInt(10 ** 6));
|
||||
console.log('✅ HEZ price:', hezPrice, 'USD');
|
||||
setHezUsdPrice(hezPrice);
|
||||
} else {
|
||||
console.warn('⚠️ wHEZ/wUSDT pool has no reserves');
|
||||
}
|
||||
|
||||
// Fetch PEZ/wUSDT pool reserves (Asset 1 / Asset 2)
|
||||
const pezPoolId = api.createType('(u32, u32)', [1, 2]);
|
||||
const pezPalletIdType = api.createType('[u8; 8]', PALLET_ID);
|
||||
const pezFullTuple = api.createType('([u8; 8], (u32, u32))', [pezPalletIdType, pezPoolId]);
|
||||
const pezAccountHash = blake2AsU8a(pezFullTuple.toU8a(), 256);
|
||||
const pezPoolAccountId = api.createType('AccountId32', pezAccountHash);
|
||||
|
||||
const pezReserve0Query = await api.query.assets.account(1, pezPoolAccountId);
|
||||
const pezReserve1Query = await api.query.assets.account(2, pezPoolAccountId);
|
||||
|
||||
if (pezReserve0Query.isSome && pezReserve1Query.isSome) {
|
||||
const reserve0Data = pezReserve0Query.unwrap();
|
||||
const reserve1Data = pezReserve1Query.unwrap();
|
||||
|
||||
const reserve0 = BigInt(reserve0Data.balance.toString()); // PEZ (12 decimals)
|
||||
const reserve1 = BigInt(reserve1Data.balance.toString()); // wUSDT (6 decimals)
|
||||
|
||||
// Calculate price: 1 PEZ = ? USD
|
||||
const pezPrice = Number(reserve1 * BigInt(10 ** 12)) / Number(reserve0 * BigInt(10 ** 6));
|
||||
console.log('✅ PEZ price:', pezPrice, 'USD');
|
||||
setPezUsdPrice(pezPrice);
|
||||
} else {
|
||||
console.warn('⚠️ PEZ/wUSDT pool has no reserves');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to fetch token prices:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch other tokens (only custom tokens - wrapped tokens are backend-only)
|
||||
const fetchOtherTokens = async () => {
|
||||
if (!api || !isApiReady || !selectedAccount) return;
|
||||
|
||||
try {
|
||||
const tokens: TokenBalance[] = [];
|
||||
|
||||
// IMPORTANT: Only show custom tokens added by user
|
||||
// Wrapped tokens (wHEZ, wUSDT) are for backend operations only
|
||||
// Core tokens (HEZ, PEZ) are shown in their own dedicated cards
|
||||
const assetIdsToCheck = customTokenIds.filter((id) =>
|
||||
id !== ASSET_IDS.WHEZ && // Exclude wrapped tokens
|
||||
id !== ASSET_IDS.WUSDT &&
|
||||
id !== ASSET_IDS.PEZ // Exclude core tokens
|
||||
);
|
||||
|
||||
for (const assetId of assetIdsToCheck) {
|
||||
try {
|
||||
const assetBalance = await api.query.assets.account(assetId, selectedAccount.address);
|
||||
const assetMetadata = await api.query.assets.metadata(assetId);
|
||||
|
||||
if (assetBalance.isSome) {
|
||||
const assetData = assetBalance.unwrap();
|
||||
const balance = assetData.balance.toString();
|
||||
|
||||
const metadata = assetMetadata.toJSON() as any;
|
||||
|
||||
// Decode hex strings properly
|
||||
let symbol = metadata.symbol || '';
|
||||
let name = metadata.name || '';
|
||||
|
||||
if (typeof symbol === 'string' && symbol.startsWith('0x')) {
|
||||
symbol = hexToString(symbol);
|
||||
}
|
||||
if (typeof name === 'string' && name.startsWith('0x')) {
|
||||
name = hexToString(name);
|
||||
}
|
||||
|
||||
// Fallback to known symbols if metadata is empty
|
||||
if (!symbol || symbol.trim() === '') {
|
||||
symbol = getAssetSymbol(assetId);
|
||||
}
|
||||
if (!name || name.trim() === '') {
|
||||
name = symbol;
|
||||
}
|
||||
|
||||
const decimals = metadata.decimals || getAssetDecimals(assetId);
|
||||
const balanceFormatted = (parseInt(balance) / Math.pow(10, decimals)).toFixed(6);
|
||||
|
||||
// Simple USD calculation (would use real price feed in production)
|
||||
let usdValue = 0;
|
||||
if (assetId === ASSET_IDS.WUSDT) {
|
||||
usdValue = parseFloat(balanceFormatted); // 1 wUSDT = 1 USD
|
||||
} else if (assetId === ASSET_IDS.WHEZ) {
|
||||
usdValue = parseFloat(balanceFormatted) * 0.5; // Placeholder price
|
||||
}
|
||||
|
||||
tokens.push({
|
||||
assetId,
|
||||
symbol: symbol.trim(),
|
||||
name: name.trim(),
|
||||
balance: balanceFormatted,
|
||||
decimals,
|
||||
usdValue
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch token ${assetId}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
setOtherTokens(tokens);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch other tokens:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchBalance = async () => {
|
||||
if (!api || !isApiReady || !selectedAccount) return;
|
||||
@@ -26,14 +219,14 @@ export const AccountBalance: React.FC = () => {
|
||||
try {
|
||||
// Fetch HEZ balance
|
||||
const { data: balanceData } = await api.query.system.account(selectedAccount.address);
|
||||
|
||||
|
||||
const free = balanceData.free.toString();
|
||||
const reserved = balanceData.reserved.toString();
|
||||
|
||||
|
||||
// Convert from plancks to tokens (12 decimals)
|
||||
const decimals = 12;
|
||||
const divisor = Math.pow(10, decimals);
|
||||
|
||||
|
||||
const freeTokens = (parseInt(free) / divisor).toFixed(4);
|
||||
const reservedTokens = (parseInt(reserved) / divisor).toFixed(4);
|
||||
const totalTokens = ((parseInt(free) + parseInt(reserved)) / divisor).toFixed(4);
|
||||
@@ -47,7 +240,7 @@ export const AccountBalance: React.FC = () => {
|
||||
// Fetch PEZ balance (Asset ID: 1)
|
||||
try {
|
||||
const pezAssetBalance = await api.query.assets.account(1, selectedAccount.address);
|
||||
|
||||
|
||||
if (pezAssetBalance.isSome) {
|
||||
const assetData = pezAssetBalance.unwrap();
|
||||
const pezAmount = assetData.balance.toString();
|
||||
@@ -60,6 +253,31 @@ export const AccountBalance: React.FC = () => {
|
||||
console.error('Failed to fetch PEZ balance:', error);
|
||||
setPezBalance('0');
|
||||
}
|
||||
|
||||
// Fetch USDT balance (wUSDT - Asset ID: 2)
|
||||
try {
|
||||
const usdtAssetBalance = await api.query.assets.account(2, selectedAccount.address);
|
||||
|
||||
if (usdtAssetBalance.isSome) {
|
||||
const assetData = usdtAssetBalance.unwrap();
|
||||
const usdtAmount = assetData.balance.toString();
|
||||
const usdtDecimals = 6; // wUSDT has 6 decimals
|
||||
const usdtDivisor = Math.pow(10, usdtDecimals);
|
||||
const usdtTokens = (parseInt(usdtAmount) / usdtDivisor).toFixed(2);
|
||||
setUsdtBalance(usdtTokens);
|
||||
} else {
|
||||
setUsdtBalance('0');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch USDT balance:', error);
|
||||
setUsdtBalance('0');
|
||||
}
|
||||
|
||||
// Fetch token prices from pools
|
||||
await fetchTokenPrices();
|
||||
|
||||
// Fetch other tokens
|
||||
await fetchOtherTokens();
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch balance:', error);
|
||||
} finally {
|
||||
@@ -67,8 +285,36 @@ export const AccountBalance: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Add custom token handler
|
||||
const handleAddToken = async (assetId: number) => {
|
||||
if (customTokenIds.includes(assetId)) {
|
||||
alert('Token already added!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Update custom tokens list
|
||||
const updatedTokenIds = [...customTokenIds, assetId];
|
||||
setCustomTokenIds(updatedTokenIds);
|
||||
localStorage.setItem('customTokenIds', JSON.stringify(updatedTokenIds));
|
||||
|
||||
// Fetch the new token
|
||||
await fetchOtherTokens();
|
||||
setIsAddTokenModalOpen(false);
|
||||
};
|
||||
|
||||
// Remove token handler
|
||||
const handleRemoveToken = (assetId: number) => {
|
||||
const updatedTokenIds = customTokenIds.filter(id => id !== assetId);
|
||||
setCustomTokenIds(updatedTokenIds);
|
||||
localStorage.setItem('customTokenIds', JSON.stringify(updatedTokenIds));
|
||||
|
||||
// Remove from displayed tokens
|
||||
setOtherTokens(prev => prev.filter(t => t.assetId !== assetId));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchBalance();
|
||||
fetchTokenPrices(); // Fetch token USD prices from pools
|
||||
|
||||
// Fetch Trust Score
|
||||
const fetchTrustScore = async () => {
|
||||
@@ -91,6 +337,7 @@ export const AccountBalance: React.FC = () => {
|
||||
// Subscribe to HEZ balance updates
|
||||
let unsubscribeHez: () => void;
|
||||
let unsubscribePez: () => void;
|
||||
let unsubscribeUsdt: () => void;
|
||||
|
||||
const subscribeBalance = async () => {
|
||||
if (!api || !isApiReady || !selectedAccount) return;
|
||||
@@ -101,10 +348,10 @@ export const AccountBalance: React.FC = () => {
|
||||
({ data: balanceData }) => {
|
||||
const free = balanceData.free.toString();
|
||||
const reserved = balanceData.reserved.toString();
|
||||
|
||||
|
||||
const decimals = 12;
|
||||
const divisor = Math.pow(10, decimals);
|
||||
|
||||
|
||||
const freeTokens = (parseInt(free) / divisor).toFixed(4);
|
||||
const reservedTokens = (parseInt(reserved) / divisor).toFixed(4);
|
||||
const totalTokens = ((parseInt(free) + parseInt(reserved)) / divisor).toFixed(4);
|
||||
@@ -138,6 +385,28 @@ export const AccountBalance: React.FC = () => {
|
||||
} catch (error) {
|
||||
console.error('Failed to subscribe to PEZ balance:', error);
|
||||
}
|
||||
|
||||
// Subscribe to USDT balance (wUSDT - Asset ID: 2)
|
||||
try {
|
||||
unsubscribeUsdt = await api.query.assets.account(
|
||||
2,
|
||||
selectedAccount.address,
|
||||
(assetBalance) => {
|
||||
if (assetBalance.isSome) {
|
||||
const assetData = assetBalance.unwrap();
|
||||
const usdtAmount = assetData.balance.toString();
|
||||
const decimals = 6; // wUSDT has 6 decimals
|
||||
const divisor = Math.pow(10, decimals);
|
||||
const usdtTokens = (parseInt(usdtAmount) / divisor).toFixed(2);
|
||||
setUsdtBalance(usdtTokens);
|
||||
} else {
|
||||
setUsdtBalance('0');
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to subscribe to USDT balance:', error);
|
||||
}
|
||||
};
|
||||
|
||||
subscribeBalance();
|
||||
@@ -145,6 +414,7 @@ export const AccountBalance: React.FC = () => {
|
||||
return () => {
|
||||
if (unsubscribeHez) unsubscribeHez();
|
||||
if (unsubscribePez) unsubscribePez();
|
||||
if (unsubscribeUsdt) unsubscribeUsdt();
|
||||
};
|
||||
}, [api, isApiReady, selectedAccount]);
|
||||
|
||||
@@ -189,7 +459,9 @@ export const AccountBalance: React.FC = () => {
|
||||
<span className="text-2xl text-gray-400 ml-2">HEZ</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
≈ ${(parseFloat(balance.total) * 0.5).toFixed(2)} USD
|
||||
{hezUsdPrice > 0
|
||||
? `≈ $${(parseFloat(balance.total) * hezUsdPrice).toFixed(2)} USD`
|
||||
: 'Price loading...'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -232,12 +504,37 @@ export const AccountBalance: React.FC = () => {
|
||||
<span className="text-2xl text-gray-400 ml-2">PEZ</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
{pezUsdPrice > 0
|
||||
? `≈ $${(parseFloat(pezBalance) * pezUsdPrice).toFixed(2)} USD`
|
||||
: 'Price loading...'}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
Governance & Rewards Token
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* USDT Balance Card */}
|
||||
<Card className="bg-gradient-to-br from-emerald-900/30 to-teal-900/30 border-emerald-500/30">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg font-medium text-gray-300">
|
||||
USDT Balance
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div>
|
||||
<div className="text-4xl font-bold text-white mb-1">
|
||||
{isLoading ? '...' : usdtBalance}
|
||||
<span className="text-2xl text-gray-400 ml-2">USDT</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
≈ ${usdtBalance} USD • Stablecoin
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Account Info */}
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardContent className="pt-6">
|
||||
@@ -266,6 +563,118 @@ export const AccountBalance: React.FC = () => {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Other Tokens */}
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Coins className="h-5 w-5 text-cyan-400" />
|
||||
<CardTitle className="text-lg font-medium text-gray-300">
|
||||
Other Assets
|
||||
</CardTitle>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setIsAddTokenModalOpen(true)}
|
||||
className="text-cyan-400 hover:text-cyan-300 hover:bg-cyan-400/10"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
Add Token
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{otherTokens.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<Coins className="w-12 h-12 text-gray-600 mx-auto mb-3 opacity-50" />
|
||||
<p className="text-gray-500 text-sm">No custom tokens yet</p>
|
||||
<p className="text-gray-600 text-xs mt-1">
|
||||
Add custom tokens to track additional assets
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{otherTokens.map((token) => {
|
||||
const tokenColor = getTokenColor(token.assetId);
|
||||
return (
|
||||
<div
|
||||
key={token.assetId}
|
||||
className="flex items-center justify-between p-4 bg-gray-800/50 rounded-lg hover:bg-gray-800/70 transition-all duration-200 group border border-transparent hover:border-gray-700"
|
||||
>
|
||||
<div className="flex items-center gap-4 flex-1">
|
||||
{/* Token Logo */}
|
||||
<div className={`w-12 h-12 rounded-full bg-gradient-to-br ${tokenColor.bg} flex items-center justify-center border ${tokenColor.border} shadow-lg`}>
|
||||
<span className={`text-base font-bold ${tokenColor.text}`}>
|
||||
{token.symbol.slice(0, 2).toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Token Info */}
|
||||
<div className="flex-1">
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-base font-semibold text-white">
|
||||
{token.symbol}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
#{token.assetId}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 mt-0.5">
|
||||
{token.name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Balance & Actions */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-right">
|
||||
<div className="text-base font-semibold text-white">
|
||||
{parseFloat(token.balance).toFixed(4)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
${token.usdValue.toFixed(2)} USD
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Send Button */}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setSelectedTokenForTransfer(token);
|
||||
setIsTransferModalOpen(true);
|
||||
}}
|
||||
className={`${tokenColor.text} hover:${tokenColor.text} hover:bg-gray-700/50 opacity-0 group-hover:opacity-100 transition-opacity`}
|
||||
>
|
||||
<Send className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Add Token Modal */}
|
||||
<AddTokenModal
|
||||
isOpen={isAddTokenModalOpen}
|
||||
onClose={() => setIsAddTokenModalOpen(false)}
|
||||
onAddToken={handleAddToken}
|
||||
/>
|
||||
|
||||
{/* Transfer Modal */}
|
||||
<TransferModal
|
||||
isOpen={isTransferModalOpen}
|
||||
onClose={() => {
|
||||
setIsTransferModalOpen(false);
|
||||
setSelectedTokenForTransfer(null);
|
||||
}}
|
||||
selectedAsset={selectedTokenForTransfer}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
|
||||
interface AddTokenModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onAddToken: (assetId: number) => Promise<void>;
|
||||
}
|
||||
|
||||
export const AddTokenModal: React.FC<AddTokenModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onAddToken,
|
||||
}) => {
|
||||
const [assetId, setAssetId] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
|
||||
const id = parseInt(assetId);
|
||||
if (isNaN(id) || id < 0) {
|
||||
setError('Please enter a valid asset ID (number)');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await onAddToken(id);
|
||||
setAssetId('');
|
||||
setError('');
|
||||
} catch (err) {
|
||||
setError('Failed to add token. Please check the asset ID and try again.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAssetId('');
|
||||
setError('');
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="bg-gray-900 border-gray-800 text-white sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl">Add Custom Token</DialogTitle>
|
||||
<DialogDescription className="text-gray-400">
|
||||
Enter the asset ID of the token you want to track.
|
||||
Note: Core tokens (HEZ, PEZ) are already displayed separately.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4 mt-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="assetId" className="text-sm text-gray-300">
|
||||
Asset ID
|
||||
</Label>
|
||||
<Input
|
||||
id="assetId"
|
||||
type="number"
|
||||
value={assetId}
|
||||
onChange={(e) => setAssetId(e.target.value)}
|
||||
placeholder="e.g., 3"
|
||||
className="bg-gray-800 border-gray-700 text-white placeholder:text-gray-500"
|
||||
min="0"
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-gray-500">
|
||||
Each token on the network has a unique asset ID
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="flex items-center gap-2 p-3 bg-red-500/10 border border-red-500/20 rounded-md">
|
||||
<AlertCircle className="h-4 w-4 text-red-400" />
|
||||
<p className="text-sm text-red-400">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={handleClose}
|
||||
disabled={isLoading}
|
||||
className="border border-gray-700 hover:bg-gray-800"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="bg-cyan-600 hover:bg-cyan-700"
|
||||
>
|
||||
{isLoading ? 'Adding...' : 'Add Token'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -5,12 +5,28 @@ import { Button } from '@/components/ui/button';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||
import { useWallet } from '@/contexts/WalletContext';
|
||||
import { ASSET_IDS } from '@/lib/wallet';
|
||||
import { ASSET_IDS, getAssetSymbol } from '@/lib/wallet';
|
||||
import { AddLiquidityModal } from '@/components/AddLiquidityModal';
|
||||
import { RemoveLiquidityModal } from '@/components/RemoveLiquidityModal';
|
||||
|
||||
// Helper function to convert asset IDs to user-friendly display names
|
||||
// Users should only see HEZ, PEZ, USDT - wrapped tokens are backend details
|
||||
const getDisplayTokenName = (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); // Fallback for other assets
|
||||
};
|
||||
|
||||
// Helper function to get decimals for each asset
|
||||
const getAssetDecimals = (assetId: number): number => {
|
||||
if (assetId === ASSET_IDS.WUSDT) return 6; // wUSDT has 6 decimals
|
||||
return 12; // wHEZ, PEZ have 12 decimals
|
||||
};
|
||||
|
||||
interface PoolData {
|
||||
asset0: number;
|
||||
asset1: number;
|
||||
@@ -38,18 +54,66 @@ const PoolDashboard = () => {
|
||||
const [isAddLiquidityModalOpen, setIsAddLiquidityModalOpen] = useState(false);
|
||||
const [isRemoveLiquidityModalOpen, setIsRemoveLiquidityModalOpen] = useState(false);
|
||||
|
||||
// Fetch pool data
|
||||
// Pool selection state
|
||||
const [availablePools, setAvailablePools] = useState<Array<[number, number]>>([]);
|
||||
const [selectedPool, setSelectedPool] = useState<string>('1-2'); // Default: PEZ/wUSDT
|
||||
|
||||
// Discover available pools
|
||||
useEffect(() => {
|
||||
if (!api || !isApiReady) return;
|
||||
|
||||
const discoverPools = async () => {
|
||||
try {
|
||||
// Check all possible pool combinations
|
||||
const possiblePools: Array<[number, number]> = [
|
||||
[ASSET_IDS.WHEZ, ASSET_IDS.PEZ], // wHEZ/PEZ
|
||||
[ASSET_IDS.WHEZ, ASSET_IDS.WUSDT], // wHEZ/wUSDT
|
||||
[ASSET_IDS.PEZ, ASSET_IDS.WUSDT], // PEZ/wUSDT
|
||||
];
|
||||
|
||||
const existingPools: Array<[number, number]> = [];
|
||||
|
||||
for (const [asset0, asset1] of possiblePools) {
|
||||
const poolInfo = await api.query.assetConversion.pools([asset0, asset1]);
|
||||
if (poolInfo.isSome) {
|
||||
existingPools.push([asset0, asset1]);
|
||||
}
|
||||
}
|
||||
|
||||
setAvailablePools(existingPools);
|
||||
|
||||
// Set default pool to first available if current selection doesn't exist
|
||||
if (existingPools.length > 0) {
|
||||
const currentPoolKey = selectedPool;
|
||||
const poolExists = existingPools.some(
|
||||
([a0, a1]) => `${a0}-${a1}` === currentPoolKey
|
||||
);
|
||||
if (!poolExists) {
|
||||
const [firstAsset0, firstAsset1] = existingPools[0];
|
||||
setSelectedPool(`${firstAsset0}-${firstAsset1}`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error discovering pools:', err);
|
||||
}
|
||||
};
|
||||
|
||||
discoverPools();
|
||||
}, [api, isApiReady]);
|
||||
|
||||
// Fetch pool data
|
||||
useEffect(() => {
|
||||
if (!api || !isApiReady || !selectedPool) return;
|
||||
|
||||
const fetchPoolData = async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Query PEZ/wUSDT pool
|
||||
const asset1 = 1; // PEZ
|
||||
const asset2 = 2; // wUSDT
|
||||
// Parse selected pool (e.g., "1-2" -> [1, 2])
|
||||
const [asset1Str, asset2Str] = selectedPool.split('-');
|
||||
const asset1 = parseInt(asset1Str);
|
||||
const asset2 = parseInt(asset2Str);
|
||||
const poolId = [asset1, asset2];
|
||||
|
||||
const poolInfo = await api.query.assetConversion.pools(poolId);
|
||||
@@ -78,25 +142,29 @@ const PoolDashboard = () => {
|
||||
const poolAccount = poolAccountId.toString();
|
||||
|
||||
// Get reserves
|
||||
const pezBalanceData = await api.query.assets.account(asset1, poolAccountId);
|
||||
const wusdtBalanceData = await api.query.assets.account(asset2, poolAccountId);
|
||||
const asset0BalanceData = await api.query.assets.account(asset1, poolAccountId);
|
||||
const asset1BalanceData = await api.query.assets.account(asset2, poolAccountId);
|
||||
|
||||
let reserve0 = 0;
|
||||
let reserve1 = 0;
|
||||
|
||||
if (pezBalanceData.isSome) {
|
||||
const pezData = pezBalanceData.unwrap().toJSON() as any;
|
||||
reserve0 = Number(pezData.balance) / 1e12;
|
||||
// Use dynamic decimals for each asset
|
||||
const asset1Decimals = getAssetDecimals(asset1);
|
||||
const asset2Decimals = getAssetDecimals(asset2);
|
||||
|
||||
if (asset0BalanceData.isSome) {
|
||||
const asset0Data = asset0BalanceData.unwrap().toJSON() as any;
|
||||
reserve0 = Number(asset0Data.balance) / Math.pow(10, asset1Decimals);
|
||||
}
|
||||
|
||||
if (wusdtBalanceData.isSome) {
|
||||
const wusdtData = wusdtBalanceData.unwrap().toJSON() as any;
|
||||
reserve1 = Number(wusdtData.balance) / 1e6; // wUSDT has 6 decimals
|
||||
if (asset1BalanceData.isSome) {
|
||||
const asset1Data = asset1BalanceData.unwrap().toJSON() as any;
|
||||
reserve1 = Number(asset1Data.balance) / Math.pow(10, asset2Decimals);
|
||||
}
|
||||
|
||||
setPoolData({
|
||||
asset0: 1,
|
||||
asset1: 2,
|
||||
asset0: asset1,
|
||||
asset1: asset2,
|
||||
reserve0,
|
||||
reserve1,
|
||||
lpTokenId,
|
||||
@@ -162,7 +230,7 @@ const PoolDashboard = () => {
|
||||
const interval = setInterval(fetchPoolData, 30000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [api, isApiReady, selectedAccount]);
|
||||
}, [api, isApiReady, selectedAccount, selectedPool]);
|
||||
|
||||
// Calculate metrics
|
||||
const constantProduct = poolData ? poolData.reserve0 * poolData.reserve1 : 0;
|
||||
@@ -221,15 +289,34 @@ const PoolDashboard = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// Get asset symbols for the selected pool (using display names)
|
||||
const asset0Symbol = poolData ? getDisplayTokenName(poolData.asset0) : '';
|
||||
const asset1Symbol = poolData ? getDisplayTokenName(poolData.asset1) : '';
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Pool Selector */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white flex items-center gap-2">
|
||||
<Droplet className="h-6 w-6 text-blue-400" />
|
||||
PEZ/wUSDT Pool Dashboard
|
||||
</h2>
|
||||
<p className="text-gray-400 mt-1">Monitor liquidity pool metrics and your position</p>
|
||||
<div className="flex items-center gap-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-400 mb-1">Pool Dashboards</h3>
|
||||
<Select value={selectedPool} onValueChange={setSelectedPool}>
|
||||
<SelectTrigger className="w-[240px] bg-gray-800/50 border-gray-700">
|
||||
<SelectValue placeholder="Select pool" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availablePools.map(([asset0, asset1]) => {
|
||||
const symbol0 = getDisplayTokenName(asset0);
|
||||
const symbol1 = getDisplayTokenName(asset1);
|
||||
return (
|
||||
<SelectItem key={`${asset0}-${asset1}`} value={`${asset0}-${asset1}`}>
|
||||
{symbol0}/{symbol1}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="outline" className="flex items-center gap-1">
|
||||
<Clock className="h-3 w-3" />
|
||||
@@ -237,6 +324,17 @@ const PoolDashboard = () => {
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Pool Dashboard Title */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white flex items-center gap-2">
|
||||
<Droplet className="h-6 w-6 text-blue-400" />
|
||||
{asset0Symbol}/{asset1Symbol} Pool Dashboard
|
||||
</h2>
|
||||
<p className="text-gray-400 mt-1">Monitor liquidity pool metrics and your position</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Key Metrics Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* Total Liquidity */}
|
||||
@@ -248,7 +346,7 @@ const PoolDashboard = () => {
|
||||
${totalLiquidityUSD.toLocaleString('en-US', { maximumFractionDigits: 0 })}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
{poolData.reserve0.toLocaleString()} PEZ + {poolData.reserve1.toLocaleString()} wUSDT
|
||||
{poolData.reserve0.toLocaleString()} {asset0Symbol} + {poolData.reserve1.toLocaleString()} {asset1Symbol}
|
||||
</p>
|
||||
</div>
|
||||
<DollarSign className="h-8 w-8 text-green-400" />
|
||||
@@ -259,12 +357,12 @@ const PoolDashboard = () => {
|
||||
<Card className="p-4 bg-gray-800/50 border-gray-700">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">PEZ Price</p>
|
||||
<p className="text-sm text-gray-400">{asset0Symbol} Price</p>
|
||||
<p className="text-2xl font-bold text-white mt-1">
|
||||
${currentPrice.toFixed(4)}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
1 wUSDT = {(1 / currentPrice).toFixed(4)} PEZ
|
||||
1 {asset1Symbol} = {(1 / currentPrice).toFixed(4)} {asset0Symbol}
|
||||
</p>
|
||||
</div>
|
||||
<TrendingUp className="h-8 w-8 text-blue-400" />
|
||||
@@ -319,7 +417,7 @@ const PoolDashboard = () => {
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-4 bg-gray-900/50 rounded-lg">
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">PEZ Reserve</p>
|
||||
<p className="text-sm text-gray-400">{asset0Symbol} Reserve</p>
|
||||
<p className="text-2xl font-bold text-white">{poolData.reserve0.toLocaleString('en-US', { maximumFractionDigits: 2 })}</p>
|
||||
</div>
|
||||
<Badge variant="outline">Asset 1</Badge>
|
||||
@@ -327,7 +425,7 @@ const PoolDashboard = () => {
|
||||
|
||||
<div className="flex items-center justify-between p-4 bg-gray-900/50 rounded-lg">
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">wUSDT Reserve</p>
|
||||
<p className="text-sm text-gray-400">{asset1Symbol} Reserve</p>
|
||||
<p className="text-2xl font-bold text-white">{poolData.reserve1.toLocaleString('en-US', { maximumFractionDigits: 2 })}</p>
|
||||
</div>
|
||||
<Badge variant="outline">Asset 2</Badge>
|
||||
@@ -387,11 +485,11 @@ const PoolDashboard = () => {
|
||||
<p className="text-sm text-gray-400 mb-2">Your Position Value</p>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-300">PEZ:</span>
|
||||
<span className="text-gray-300">{asset0Symbol}:</span>
|
||||
<span className="text-white font-semibold">{lpPosition.asset0Amount.toFixed(4)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-300">wUSDT:</span>
|
||||
<span className="text-gray-300">{asset1Symbol}:</span>
|
||||
<span className="text-white font-semibold">{lpPosition.asset1Amount.toFixed(4)}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -402,15 +500,15 @@ const PoolDashboard = () => {
|
||||
<div className="space-y-1 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-300">Daily:</span>
|
||||
<span className="text-green-400">~{((lpPosition.asset0Amount * 2 * estimateAPR()) / 365 / 100).toFixed(4)} HEZ</span>
|
||||
<span className="text-green-400">~{((lpPosition.asset0Amount * 2 * estimateAPR()) / 365 / 100).toFixed(4)} {asset0Symbol}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-300">Monthly:</span>
|
||||
<span className="text-green-400">~{((lpPosition.asset0Amount * 2 * estimateAPR()) / 12 / 100).toFixed(4)} HEZ</span>
|
||||
<span className="text-green-400">~{((lpPosition.asset0Amount * 2 * estimateAPR()) / 12 / 100).toFixed(4)} {asset0Symbol}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-300">Yearly:</span>
|
||||
<span className="text-green-400">~{((lpPosition.asset0Amount * 2 * estimateAPR()) / 100).toFixed(4)} HEZ</span>
|
||||
<span className="text-green-400">~{((lpPosition.asset0Amount * 2 * estimateAPR()) / 100).toFixed(4)} {asset0Symbol}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -442,7 +540,7 @@ const PoolDashboard = () => {
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-gray-900/50 rounded-lg">
|
||||
<p className="text-sm text-gray-400 mb-3">If PEZ price changes by:</p>
|
||||
<p className="text-sm text-gray-400 mb-3">If {asset0Symbol} price changes by:</p>
|
||||
|
||||
<div className="space-y-2">
|
||||
{[10, 25, 50, 100, 200].map((change) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -20,9 +20,19 @@ import {
|
||||
import { ArrowRight, Loader2, CheckCircle, XCircle } from 'lucide-react';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
|
||||
interface TokenBalance {
|
||||
assetId: number;
|
||||
symbol: string;
|
||||
name: string;
|
||||
balance: string;
|
||||
decimals: number;
|
||||
usdValue: number;
|
||||
}
|
||||
|
||||
interface TransferModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
selectedAsset?: TokenBalance | null;
|
||||
}
|
||||
|
||||
type TokenType = 'HEZ' | 'PEZ' | 'USDT' | 'BTC' | 'ETH' | 'DOT';
|
||||
@@ -44,10 +54,10 @@ const TOKENS: Token[] = [
|
||||
{ symbol: 'DOT', name: 'Polkadot', assetId: 5, decimals: 10, color: 'from-pink-500 to-red-500' },
|
||||
];
|
||||
|
||||
export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose }) => {
|
||||
export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose, selectedAsset }) => {
|
||||
const { api, isApiReady, selectedAccount } = usePolkadot();
|
||||
const { toast } = useToast();
|
||||
|
||||
|
||||
const [selectedToken, setSelectedToken] = useState<TokenType>('HEZ');
|
||||
const [recipient, setRecipient] = useState('');
|
||||
const [amount, setAmount] = useState('');
|
||||
@@ -55,7 +65,16 @@ export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose })
|
||||
const [txStatus, setTxStatus] = useState<'idle' | 'signing' | 'pending' | 'success' | 'error'>('idle');
|
||||
const [txHash, setTxHash] = useState('');
|
||||
|
||||
const currentToken = TOKENS.find(t => t.symbol === selectedToken) || TOKENS[0];
|
||||
// Use the provided selectedAsset or fall back to token selection
|
||||
const currentToken = selectedAsset ? {
|
||||
symbol: selectedAsset.symbol as TokenType,
|
||||
name: selectedAsset.name,
|
||||
assetId: selectedAsset.assetId,
|
||||
decimals: selectedAsset.decimals,
|
||||
color: selectedAsset.assetId === 0 ? 'from-green-600 to-yellow-400' :
|
||||
selectedAsset.assetId === 2 ? 'from-emerald-500 to-teal-500' :
|
||||
'from-cyan-500 to-blue-500',
|
||||
} : TOKENS.find(t => t.symbol === selectedToken) || TOKENS[0];
|
||||
|
||||
const handleTransfer = async () => {
|
||||
if (!api || !isApiReady || !selectedAccount) {
|
||||
@@ -90,14 +109,12 @@ export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose })
|
||||
let transfer;
|
||||
|
||||
// Create appropriate transfer transaction based on token type
|
||||
if (selectedToken === 'HEZ') {
|
||||
// Native token transfer
|
||||
// wHEZ uses native token transfer (balances pallet), all others use assets pallet
|
||||
if (currentToken.assetId === undefined || (selectedToken === 'HEZ' && !selectedAsset)) {
|
||||
// Native HEZ token transfer
|
||||
transfer = api.tx.balances.transferKeepAlive(recipient, amountInSmallestUnit.toString());
|
||||
} else {
|
||||
// Asset token transfer (PEZ, USDT, BTC, ETH, DOT)
|
||||
if (!currentToken.assetId) {
|
||||
throw new Error('Asset ID not configured');
|
||||
}
|
||||
// Asset token transfer (wHEZ, PEZ, wUSDT, etc.)
|
||||
transfer = api.tx.assets.transfer(currentToken.assetId, recipient, amountInSmallestUnit.toString());
|
||||
}
|
||||
|
||||
@@ -135,7 +152,7 @@ export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose })
|
||||
setTxStatus('success');
|
||||
toast({
|
||||
title: "Transfer Successful!",
|
||||
description: `Sent ${amount} ${selectedToken} to ${recipient.slice(0, 8)}...${recipient.slice(-6)}`,
|
||||
description: `Sent ${amount} ${currentToken.symbol} to ${recipient.slice(0, 8)}...${recipient.slice(-6)}`,
|
||||
});
|
||||
|
||||
// Reset form after 2 seconds
|
||||
@@ -181,9 +198,13 @@ export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose })
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="bg-gray-900 border-gray-800">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">Send Tokens</DialogTitle>
|
||||
<DialogTitle className="text-white">
|
||||
{selectedAsset ? `Send ${selectedAsset.symbol}` : 'Send Tokens'}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-gray-400">
|
||||
Transfer tokens to another account
|
||||
{selectedAsset
|
||||
? `Transfer ${selectedAsset.name} to another account`
|
||||
: 'Transfer tokens to another account'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -215,30 +236,32 @@ export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose })
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{/* Token Selection */}
|
||||
<div>
|
||||
<Label htmlFor="token" className="text-white">Select Token</Label>
|
||||
<Select value={selectedToken} onValueChange={(value) => setSelectedToken(value as TokenType)} disabled={isTransferring}>
|
||||
<SelectTrigger className="bg-gray-800 border-gray-700 text-white mt-2">
|
||||
<SelectValue placeholder="Select token" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-gray-800 border-gray-700">
|
||||
{TOKENS.map((token) => (
|
||||
<SelectItem
|
||||
key={token.symbol}
|
||||
value={token.symbol}
|
||||
className="text-white hover:bg-gray-700 focus:bg-gray-700"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-3 h-3 rounded-full bg-gradient-to-r ${token.color}`}></div>
|
||||
<span className="font-semibold">{token.symbol}</span>
|
||||
<span className="text-gray-400 text-sm">- {token.name}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{/* Token Selection - Only show if no asset is pre-selected */}
|
||||
{!selectedAsset && (
|
||||
<div>
|
||||
<Label htmlFor="token" className="text-white">Select Token</Label>
|
||||
<Select value={selectedToken} onValueChange={(value) => setSelectedToken(value as TokenType)} disabled={isTransferring}>
|
||||
<SelectTrigger className="bg-gray-800 border-gray-700 text-white mt-2">
|
||||
<SelectValue placeholder="Select token" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-gray-800 border-gray-700">
|
||||
{TOKENS.map((token) => (
|
||||
<SelectItem
|
||||
key={token.symbol}
|
||||
value={token.symbol}
|
||||
className="text-white hover:bg-gray-700 focus:bg-gray-700"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-3 h-3 rounded-full bg-gradient-to-r ${token.color}`}></div>
|
||||
<span className="font-semibold">{token.symbol}</span>
|
||||
<span className="text-gray-400 text-sm">- {token.name}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Label htmlFor="recipient" className="text-white">Recipient Address</Label>
|
||||
|
||||
@@ -98,7 +98,7 @@ export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
||||
console.error('❌ Failed to fetch wHEZ balance:', err);
|
||||
}
|
||||
|
||||
// Fetch wUSDT (Asset ID: 2)
|
||||
// Fetch wUSDT (Asset ID: 2) - IMPORTANT: wUSDT has 6 decimals, not 12!
|
||||
let wusdtBalance = '0';
|
||||
try {
|
||||
const wusdtData = await polkadot.api.query.assets.account(ASSET_IDS.WUSDT, address);
|
||||
@@ -107,7 +107,7 @@ export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
||||
if (wusdtData.isSome) {
|
||||
const assetData = wusdtData.unwrap();
|
||||
const wusdtAmount = assetData.balance.toString();
|
||||
wusdtBalance = formatBalance(wusdtAmount);
|
||||
wusdtBalance = formatBalance(wusdtAmount, 6); // wUSDT uses 6 decimals!
|
||||
console.log('✅ wUSDT balance found:', wusdtBalance);
|
||||
} else {
|
||||
console.warn('⚠️ wUSDT asset not found for this account');
|
||||
|
||||
Reference in New Issue
Block a user