Files
pwap/web/src/components/AccountBalance.tsx
T
pezkuwichain d82ffaca74 fix: display wHEZ, wUSDT and LP tokens in AccountBalance
- Fix wUSDT balance to use Asset Hub API instead of relay chain
- Fix USDT subscription to use correct asset ID (1000)
- Add wHEZ balance display when balance > 0
- Add LP token positions display from poolAssets pallet
- Add LP token logo to public/tokens
2026-02-05 00:50:40 +03:00

1102 lines
44 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useEffect, useState } from 'react';
import { usePezkuwi } from '@/contexts/PezkuwiContext';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Wallet, TrendingUp, RefreshCw, Award, Plus, Coins, Send, Shield, Users, Fuel } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { ASSET_IDS, getAssetSymbol } from '@pezkuwi/lib/wallet';
import { AddTokenModal } from './AddTokenModal';
import { TransferModal } from './TransferModal';
import { XCMTeleportModal } from './XCMTeleportModal';
import { getAllScores, type UserScores } from '@pezkuwi/lib/scores';
interface TokenBalance {
assetId: number;
symbol: string;
name: string;
balance: string;
decimals: number;
usdValue: number;
isLpToken?: boolean; // LP tokens from poolAssets pallet
}
export const AccountBalance: React.FC = () => {
const { api, assetHubApi, peopleApi, isApiReady, isAssetHubReady, isPeopleReady, selectedAccount } = usePezkuwi();
const [balance, setBalance] = useState<{
free: string;
reserved: string;
total: string;
}>({
free: '0',
reserved: '0',
total: '0',
});
// HEZ balances on different chains
const [assetHubHezBalance, setAssetHubHezBalance] = useState<string>('0');
const [peopleHezBalance, setPeopleHezBalance] = useState<string>('0');
const [pezBalance, setPezBalance] = useState<string>('0');
const [usdtBalance, setUsdtBalance] = useState<string>('0');
const [whezBalance, setWhezBalance] = useState<string>('0');
const [lpTokens, setLpTokens] = useState<TokenBalance[]>([]);
const [hezUsdPrice, setHezUsdPrice] = useState<number>(0);
const [pezUsdPrice, setPezUsdPrice] = useState<number>(0);
const [scores, setScores] = useState<UserScores>({
trustScore: 0,
referralScore: 0,
stakingScore: 0,
tikiScore: 0,
totalScore: 0
});
const [loadingScores, setLoadingScores] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [otherTokens, setOtherTokens] = useState<TokenBalance[]>([]);
const [isAddTokenModalOpen, setIsAddTokenModalOpen] = useState(false);
const [isTransferModalOpen, setIsTransferModalOpen] = useState(false);
const [isXCMTeleportModalOpen, setIsXCMTeleportModalOpen] = 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 '';
}
};
// Token logo mapping
const TOKEN_LOGOS: Record<string, string> = {
HEZ: '/tokens/HEZ.png',
PEZ: '/tokens/PEZ.png',
USDT: '/tokens/USDT.png',
wUSDT: '/tokens/USDT.png',
wHEZ: '/tokens/HEZ.png', // wHEZ uses same logo as HEZ
BNB: '/tokens/BNB.png',
BTC: '/tokens/BTC.png',
DOT: '/tokens/DOT.png',
ETH: '/tokens/ETH.png',
'HEZ-PEZ LP': '/tokens/LP.png',
'HEZ-USDT LP': '/tokens/LP.png',
};
// Get token logo URL
const getTokenLogo = (symbol: string): string | null => {
return TOKEN_LOGOS[symbol] || TOKEN_LOGOS[symbol.toUpperCase()] || null;
};
// 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 CoinGecko with fallback logic
// Priority: CoinGecko direct > DOT-based calculation > DEX pool
const fetchTokenPrices = async () => {
try {
if (import.meta.env.DEV) console.log('💰 Fetching token prices from CoinGecko...');
// CoinGecko API - fetch DOT, HEZ, PEZ prices
// Note: HEZ and PEZ may not be listed yet, so we use DOT as fallback
const coingeckoIds = 'polkadot,pezkuwichain,pez-token'; // DOT is always available
// Use our proxy to avoid CORS and rate limits
const response = await fetch(
`https://api.pezkuwichain.io/api/prices?ids=${coingeckoIds}&vs_currencies=usd&include_24hr_change=true`
);
let hezPrice = 0;
let pezPrice = 0;
if (response.ok) {
const data = await response.json();
if (import.meta.env.DEV) console.log('📊 CoinGecko response:', data);
const dotPrice = data['polkadot']?.usd || 0;
const directHezPrice = data['pezkuwichain']?.usd || 0;
const directPezPrice = data['pez-token']?.usd || 0;
// Use direct CoinGecko price if available, otherwise calculate from DOT
if (directHezPrice > 0) {
hezPrice = directHezPrice;
if (import.meta.env.DEV) console.log('✅ HEZ price from CoinGecko:', hezPrice, 'USD');
} else if (dotPrice > 0) {
// HEZ = DOT / 3
hezPrice = dotPrice / 3;
if (import.meta.env.DEV) console.log('✅ HEZ price (DOT/3):', hezPrice, 'USD');
}
if (directPezPrice > 0) {
pezPrice = directPezPrice;
if (import.meta.env.DEV) console.log('✅ PEZ price from CoinGecko:', pezPrice, 'USD');
} else if (dotPrice > 0) {
// PEZ = DOT / 10
pezPrice = dotPrice / 10;
if (import.meta.env.DEV) console.log('✅ PEZ price (DOT/10):', pezPrice, 'USD');
}
}
// If CoinGecko failed or returned 0, try DEX pool as fallback
if ((hezPrice === 0 || pezPrice === 0) && api && isApiReady) {
if (import.meta.env.DEV) console.log('⚠️ CoinGecko incomplete, trying DEX pool fallback...');
await fetchDexPoolPrices(hezPrice, pezPrice);
} else {
setHezUsdPrice(hezPrice);
setPezUsdPrice(pezPrice);
}
} catch (error) {
if (import.meta.env.DEV) console.error('❌ CoinGecko fetch failed, trying DEX pool:', error);
// Fallback to DEX pool prices
if (api && isApiReady) {
await fetchDexPoolPrices(0, 0);
}
}
};
// Fallback: Fetch prices from DEX pools
const fetchDexPoolPrices = async (existingHezPrice: number, existingPezPrice: number) => {
if (!api || !isApiReady) return;
try {
const { stringToU8a } = await import('@pezkuwi/util');
const { blake2AsU8a } = await import('@pezkuwi/util-crypto');
const PALLET_ID = stringToU8a('py/ascon');
let hezPrice = existingHezPrice;
let pezPrice = existingPezPrice;
// Only fetch HEZ from DEX if not already set
if (hezPrice === 0) {
const whezPoolId = api.createType('(u32, u32)', [ASSET_IDS.WHEZ, ASSET_IDS.WUSDT]);
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(ASSET_IDS.WHEZ, whezPoolAccountId);
const whezReserve1Query = await api.query.assets.account(ASSET_IDS.WUSDT, whezPoolAccountId);
if (whezReserve0Query.isSome && whezReserve1Query.isSome) {
const reserve0Data = whezReserve0Query.unwrap();
const reserve1Data = whezReserve1Query.unwrap();
const reserve0 = BigInt(reserve0Data.balance.toString());
const reserve1 = BigInt(reserve1Data.balance.toString());
hezPrice = Number(reserve1 * BigInt(10 ** 12)) / Number(reserve0 * BigInt(10 ** 6));
if (import.meta.env.DEV) console.log('✅ HEZ price from DEX:', hezPrice, 'USD');
}
}
// Only fetch PEZ from DEX if not already set
if (pezPrice === 0) {
const pezPoolId = api.createType('(u32, u32)', [1, ASSET_IDS.WUSDT]);
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(ASSET_IDS.WUSDT, pezPoolAccountId);
if (pezReserve0Query.isSome && pezReserve1Query.isSome) {
const reserve0Data = pezReserve0Query.unwrap();
const reserve1Data = pezReserve1Query.unwrap();
const reserve0 = BigInt(reserve0Data.balance.toString());
const reserve1 = BigInt(reserve1Data.balance.toString());
pezPrice = Number(reserve1 * BigInt(10 ** 12)) / Number(reserve0 * BigInt(10 ** 6));
if (import.meta.env.DEV) console.log('✅ PEZ price from DEX:', pezPrice, 'USD');
}
}
setHezUsdPrice(hezPrice);
setPezUsdPrice(pezPrice);
} catch (error) {
if (import.meta.env.DEV) console.error('❌ DEX pool price fetch failed:', 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 { symbol?: string; name?: string; decimals?: number };
// 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) {
if (import.meta.env.DEV) console.error(`Failed to fetch token ${assetId}:`, error);
}
}
setOtherTokens(tokens);
} catch (error) {
if (import.meta.env.DEV) console.error('Failed to fetch other tokens:', error);
}
};
const fetchBalance = async () => {
if (!api || !isApiReady || !selectedAccount) return;
setIsLoading(true);
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);
setBalance({
free: freeTokens,
reserved: reservedTokens,
total: totalTokens,
});
// Fetch HEZ balance on Asset Hub (for PEZ transfer fees)
try {
if (assetHubApi && isAssetHubReady) {
const { data: assetHubBalanceData } = await assetHubApi.query.system.account(selectedAccount.address);
const assetHubFree = assetHubBalanceData.free.toString();
const assetHubHezTokens = (parseInt(assetHubFree) / divisor).toFixed(4);
setAssetHubHezBalance(assetHubHezTokens);
} else {
setAssetHubHezBalance('0.0000');
}
} catch (error) {
if (import.meta.env.DEV) console.error('Failed to fetch Asset Hub HEZ balance:', error);
setAssetHubHezBalance('0.0000');
}
// Fetch HEZ balance on People Chain (for identity/KYC fees)
try {
if (peopleApi && isPeopleReady) {
const { data: peopleBalanceData } = await peopleApi.query.system.account(selectedAccount.address);
const peopleFree = peopleBalanceData.free.toString();
const peopleHezTokens = (parseInt(peopleFree) / divisor).toFixed(4);
setPeopleHezBalance(peopleHezTokens);
} else {
setPeopleHezBalance('0.0000');
}
} catch (error) {
if (import.meta.env.DEV) console.error('Failed to fetch People Chain HEZ balance:', error);
setPeopleHezBalance('0.0000');
}
// Fetch PEZ balance (Asset ID: 1) from Asset Hub
try {
if (assetHubApi && isAssetHubReady) {
const pezAssetBalance = await assetHubApi.query.assets.account(1, selectedAccount.address);
if (pezAssetBalance.isSome) {
const assetData = pezAssetBalance.unwrap();
const pezAmount = assetData.balance.toString();
const pezTokens = (parseInt(pezAmount) / divisor).toFixed(4);
setPezBalance(pezTokens);
} else {
setPezBalance('0.0000');
}
} else {
if (import.meta.env.DEV) console.log('Asset Hub not ready, PEZ balance pending...');
setPezBalance('0.0000');
}
} catch (error) {
if (import.meta.env.DEV) console.error('Failed to fetch PEZ balance from Asset Hub:', error);
setPezBalance('0.0000');
}
// Fetch USDT balance (wUSDT - Asset ID: 1000) from Asset Hub
try {
if (assetHubApi && isAssetHubReady) {
const usdtAssetBalance = await assetHubApi.query.assets.account(ASSET_IDS.WUSDT, 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');
}
} else {
if (import.meta.env.DEV) console.log('Asset Hub not ready, wUSDT balance pending...');
setUsdtBalance('0');
}
} catch (error) {
if (import.meta.env.DEV) console.error('Failed to fetch USDT balance:', error);
setUsdtBalance('0');
}
// Fetch wHEZ balance (Asset ID: 2) from Asset Hub
try {
if (assetHubApi && isAssetHubReady) {
const whezAssetBalance = await assetHubApi.query.assets.account(ASSET_IDS.WHEZ, selectedAccount.address);
if (whezAssetBalance.isSome) {
const assetData = whezAssetBalance.unwrap();
const whezAmount = assetData.balance.toString();
const whezTokens = (parseInt(whezAmount) / divisor).toFixed(4);
setWhezBalance(whezTokens);
} else {
setWhezBalance('0');
}
} else {
if (import.meta.env.DEV) console.log('Asset Hub not ready, wHEZ balance pending...');
setWhezBalance('0');
}
} catch (error) {
if (import.meta.env.DEV) console.error('Failed to fetch wHEZ balance:', error);
setWhezBalance('0');
}
// Fetch LP Token balances from poolAssets pallet on Asset Hub
try {
if (assetHubApi && isAssetHubReady) {
const lpTokensData: TokenBalance[] = [];
// HEZ-PEZ LP Token (ID: 0)
const hezPezLp = await assetHubApi.query.poolAssets.account(0, selectedAccount.address);
if (hezPezLp.isSome) {
const lpBalance = hezPezLp.unwrap().balance.toString();
const lpTokens = (parseInt(lpBalance) / divisor).toFixed(4);
if (parseFloat(lpTokens) > 0) {
lpTokensData.push({
assetId: 0,
symbol: 'HEZ-PEZ LP',
name: 'HEZ-PEZ Liquidity',
balance: lpTokens,
decimals: 12,
usdValue: 0, // TODO: Calculate LP value
isLpToken: true,
});
}
}
// HEZ-USDT LP Token (ID: 1)
const hezUsdtLp = await assetHubApi.query.poolAssets.account(1, selectedAccount.address);
if (hezUsdtLp.isSome) {
const lpBalance = hezUsdtLp.unwrap().balance.toString();
const lpTokens = (parseInt(lpBalance) / divisor).toFixed(4);
if (parseFloat(lpTokens) > 0) {
lpTokensData.push({
assetId: 1,
symbol: 'HEZ-USDT LP',
name: 'HEZ-USDT Liquidity',
balance: lpTokens,
decimals: 12,
usdValue: 0, // TODO: Calculate LP value
isLpToken: true,
});
}
}
setLpTokens(lpTokensData);
if (import.meta.env.DEV) console.log('✅ LP tokens fetched:', lpTokensData);
}
} catch (error) {
if (import.meta.env.DEV) console.error('Failed to fetch LP token balances:', error);
setLpTokens([]);
}
// Fetch token prices from pools
await fetchTokenPrices();
// Fetch other tokens
await fetchOtherTokens();
} catch (error) {
if (import.meta.env.DEV) console.error('Failed to fetch balance:', error);
} finally {
setIsLoading(false);
}
};
// 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 (unused but kept for future feature)
// 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 All Scores from blockchain
const fetchAllScores = async () => {
if (!api || !isApiReady || !selectedAccount?.address) {
setScores({
trustScore: 0,
referralScore: 0,
stakingScore: 0,
tikiScore: 0,
totalScore: 0
});
return;
}
setLoadingScores(true);
try {
const userScores = await getAllScores(api, selectedAccount.address);
setScores(userScores);
} catch (err) {
if (import.meta.env.DEV) console.error('Failed to fetch scores:', err);
setScores({
trustScore: 0,
referralScore: 0,
stakingScore: 0,
tikiScore: 0,
totalScore: 0
});
} finally {
setLoadingScores(false);
}
};
fetchAllScores();
// Subscribe to HEZ balance updates
let unsubscribeHez: () => void;
let unsubscribePez: () => void;
let unsubscribeUsdt: () => void;
const subscribeBalance = async () => {
if (!api || !isApiReady || !selectedAccount) return;
// Subscribe to HEZ balance
unsubscribeHez = await api.query.system.account(
selectedAccount.address,
({ 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);
setBalance({
free: freeTokens,
reserved: reservedTokens,
total: totalTokens,
});
}
);
// Subscribe to PEZ balance (Asset ID: 1) from Asset Hub
if (assetHubApi && isAssetHubReady) {
try {
unsubscribePez = await assetHubApi.query.assets.account(
1,
selectedAccount.address,
(assetBalance) => {
if (assetBalance.isSome) {
const assetData = assetBalance.unwrap();
const pezAmount = assetData.balance.toString();
const decimals = 12;
const divisor = Math.pow(10, decimals);
const pezTokens = (parseInt(pezAmount) / divisor).toFixed(4);
setPezBalance(pezTokens);
} else {
setPezBalance('0.0000');
}
}
);
} catch (error) {
if (import.meta.env.DEV) console.error('Failed to subscribe to PEZ balance from Asset Hub:', error);
}
}
// Subscribe to USDT balance (wUSDT - Asset ID: 1000) from Asset Hub
if (assetHubApi && isAssetHubReady) {
try {
unsubscribeUsdt = await assetHubApi.query.assets.account(
ASSET_IDS.WUSDT,
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) {
if (import.meta.env.DEV) console.error('Failed to subscribe to USDT balance:', error);
}
}
};
subscribeBalance();
return () => {
if (unsubscribeHez) unsubscribeHez();
if (unsubscribePez) unsubscribePez();
if (unsubscribeUsdt) unsubscribeUsdt();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [api, assetHubApi, peopleApi, isApiReady, isAssetHubReady, isPeopleReady, selectedAccount]);
if (!selectedAccount) {
return (
<Card className="bg-gray-900 border-gray-800">
<CardContent className="pt-6">
<div className="text-center text-gray-400">
<Wallet className="w-12 h-12 mx-auto mb-3 opacity-50" />
<p>Connect your wallet to view balance</p>
</div>
</CardContent>
</Card>
);
}
return (
<div className="space-y-4">
{/* HEZ Balance Card - Multi-Chain */}
<Card className="bg-gradient-to-br from-green-900/30 to-yellow-900/30 border-green-500/30">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<img src="/tokens/HEZ.png" alt="HEZ" className="w-10 h-10 rounded-full" />
<div>
<CardTitle className="text-lg font-medium text-gray-300">
HEZ Balance
</CardTitle>
<div className="text-xs text-gray-500">Multi-Chain Overview</div>
</div>
</div>
<div className="flex items-center gap-2">
<Button
size="sm"
variant="outline"
onClick={() => setIsXCMTeleportModalOpen(true)}
className="border-yellow-500/50 text-yellow-400 hover:bg-yellow-500/10 group relative"
title="Send HEZ to teyrcahins for transaction fees"
>
<Fuel className="w-4 h-4 mr-1" />
Add Fee
<span className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 text-xs bg-gray-800 text-gray-200 rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-10">
Send HEZ to Asset Hub / People Chain
</span>
</Button>
<Button
variant="ghost"
size="icon"
onClick={fetchBalance}
disabled={isLoading}
className="text-gray-400 hover:text-white"
>
<RefreshCw className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
{/* Total HEZ */}
<div>
<div className="text-4xl font-bold text-white mb-1">
{isLoading ? '...' : (parseFloat(balance.total) + parseFloat(assetHubHezBalance) + parseFloat(peopleHezBalance)).toFixed(4)}
<span className="text-2xl text-gray-400 ml-2">HEZ</span>
</div>
<div className="text-sm text-gray-400">
{hezUsdPrice > 0
? `$${((parseFloat(balance.total) + parseFloat(assetHubHezBalance) + parseFloat(peopleHezBalance)) * hezUsdPrice).toFixed(2)} USD (Toplam)`
: 'Price loading...'}
</div>
</div>
{/* Chain Balances */}
<div className="grid grid-cols-1 gap-3">
{/* Relay Chain (Main) */}
<div className="bg-gray-800/50 rounded-lg p-3 border border-green-500/20">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-500"></div>
<span className="text-sm text-gray-300">Pezkuwi (Relay Chain)</span>
</div>
<div className="text-right">
<div className="text-lg font-semibold text-white">{balance.free} HEZ</div>
<div className="text-xs text-gray-500">Reserved: {balance.reserved}</div>
</div>
</div>
</div>
{/* Asset Hub */}
<div className="bg-gray-800/50 rounded-lg p-3 border border-blue-500/20">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-blue-500"></div>
<span className="text-sm text-gray-300">Pezkuwi Asset Hub</span>
<span className="text-xs text-gray-500">(PEZ fees)</span>
</div>
<div className="text-right">
<div className="text-lg font-semibold text-white">{assetHubHezBalance} HEZ</div>
{parseFloat(assetHubHezBalance) < 0.1 && (
<div className="text-xs text-yellow-400"> Low for fees</div>
)}
</div>
</div>
</div>
{/* People Chain */}
<div className="bg-gray-800/50 rounded-lg p-3 border border-purple-500/20">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-purple-500"></div>
<span className="text-sm text-gray-300">Pezkuwi People</span>
<span className="text-xs text-gray-500">(Identity fees)</span>
</div>
<div className="text-right">
<div className="text-lg font-semibold text-white">{peopleHezBalance} HEZ</div>
{parseFloat(peopleHezBalance) < 0.1 && (
<div className="text-xs text-yellow-400"> Low for fees</div>
)}
</div>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
{/* PEZ Balance Card */}
<Card className="bg-gradient-to-br from-blue-900/30 to-purple-900/30 border-blue-500/30">
<CardHeader className="pb-3">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-3 min-w-0">
<img src="/tokens/PEZ.png" alt="PEZ" className="w-10 h-10 rounded-full flex-shrink-0" />
<CardTitle className="text-lg font-medium text-gray-300 whitespace-nowrap">
PEZ Balance
</CardTitle>
</div>
<Button
size="sm"
variant="outline"
onClick={() => setIsXCMTeleportModalOpen(true)}
className="border-yellow-500/50 text-yellow-400 hover:bg-yellow-500/10 group relative"
title="Send HEZ to Asset Hub for transaction fees"
>
<Fuel className="w-4 h-4 mr-1" />
Add Fees
<span className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 text-xs bg-gray-800 text-gray-200 rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none">
Send HEZ for PEZ transfer fees
</span>
</Button>
</div>
</CardHeader>
<CardContent>
<div>
<div className="text-4xl font-bold text-white mb-1">
{isLoading ? '...' : pezBalance}
<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 (on Asset Hub)
</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">
<div className="flex items-center gap-3">
<img src="/tokens/USDT.png" alt="USDT" className="w-10 h-10 rounded-full" />
<CardTitle className="text-lg font-medium text-gray-300">
USDT Balance
</CardTitle>
</div>
</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 (on Asset Hub)
</div>
</div>
</CardContent>
</Card>
{/* wHEZ Balance Card */}
{parseFloat(whezBalance) > 0 && (
<Card className="bg-gradient-to-br from-yellow-900/30 to-orange-900/30 border-yellow-500/30">
<CardHeader className="pb-3">
<div className="flex items-center gap-3">
<img src="/tokens/HEZ.png" alt="wHEZ" className="w-10 h-10 rounded-full" />
<CardTitle className="text-lg font-medium text-gray-300">
wHEZ Balance
</CardTitle>
</div>
</CardHeader>
<CardContent>
<div>
<div className="text-4xl font-bold text-white mb-1">
{isLoading ? '...' : whezBalance}
<span className="text-2xl text-gray-400 ml-2">wHEZ</span>
</div>
<div className="text-sm text-gray-400">
{hezUsdPrice > 0
? `$${(parseFloat(whezBalance) * hezUsdPrice).toFixed(2)} USD`
: 'Price loading...'} Wrapped HEZ (on Asset Hub)
</div>
</div>
</CardContent>
</Card>
)}
{/* LP Token Cards */}
{lpTokens.length > 0 && (
<Card className="bg-gradient-to-br from-purple-900/30 to-pink-900/30 border-purple-500/30">
<CardHeader className="pb-3">
<div className="flex items-center gap-3">
<img src="/tokens/LP.png" alt="LP" className="w-10 h-10 rounded-full" />
<CardTitle className="text-lg font-medium text-gray-300">
LP Token Positions
</CardTitle>
</div>
</CardHeader>
<CardContent>
<div className="space-y-3">
{lpTokens.map((lp) => (
<div key={lp.assetId} className="flex items-center justify-between p-3 bg-gray-800/50 rounded-lg">
<div className="flex items-center gap-3">
<img src="/tokens/LP.png" alt={lp.symbol} className="w-8 h-8 rounded-full" />
<div>
<div className="text-sm font-medium text-white">{lp.symbol}</div>
<div className="text-xs text-gray-400">{lp.name}</div>
</div>
</div>
<div className="text-right">
<div className="text-lg font-semibold text-white">{lp.balance}</div>
<div className="text-xs text-gray-500">Pool Share</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
)}
{/* Account Info & Scores */}
<Card className="bg-gray-900 border-gray-800">
<CardHeader className="pb-3">
<CardTitle className="text-lg font-medium text-gray-300">
Account Information
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{/* Account Details */}
<div className="space-y-2 pb-4 border-b border-gray-800">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-400">Account</span>
<span className="text-white font-mono">
{selectedAccount.meta.name || 'Unnamed'}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-400">Address</span>
<span className="text-white font-mono text-xs">
{selectedAccount.address.slice(0, 8)}...{selectedAccount.address.slice(-8)}
</span>
</div>
</div>
{/* Scores from Blockchain */}
<div>
<div className="text-xs text-gray-400 mb-3">Scores from Blockchain</div>
{loadingScores ? (
<div className="text-sm text-gray-400">Loading scores...</div>
) : (
<div className="space-y-3">
{/* Score Grid */}
<div className="grid grid-cols-2 gap-3">
<div className="bg-gray-800/50 rounded-lg p-3">
<div className="flex items-center gap-1 mb-1">
<Shield className="h-3 w-3 text-purple-400" />
<span className="text-xs text-gray-400">Trust</span>
</div>
<span className="text-base font-bold text-purple-400">{scores.trustScore}</span>
</div>
<div className="bg-gray-800/50 rounded-lg p-3">
<div className="flex items-center gap-1 mb-1">
<Users className="h-3 w-3 text-cyan-400" />
<span className="text-xs text-gray-400">Referral</span>
</div>
<span className="text-base font-bold text-cyan-400">{scores.referralScore}</span>
</div>
<div className="bg-gray-800/50 rounded-lg p-3">
<div className="flex items-center gap-1 mb-1">
<TrendingUp className="h-3 w-3 text-green-400" />
<span className="text-xs text-gray-400">Staking</span>
</div>
<span className="text-base font-bold text-green-400">{scores.stakingScore}</span>
</div>
<div className="bg-gray-800/50 rounded-lg p-3">
<div className="flex items-center gap-1 mb-1">
<Award className="h-3 w-3 text-pink-400" />
<span className="text-xs text-gray-400">Tiki</span>
</div>
<span className="text-base font-bold text-pink-400">{scores.tikiScore}</span>
</div>
</div>
{/* Total Score */}
<div className="pt-3 border-t border-gray-800">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-400">Total Score</span>
<span className="text-xl font-bold bg-gradient-to-r from-purple-400 to-cyan-400 bg-clip-text text-transparent">
{scores.totalScore}
</span>
</div>
</div>
</div>
)}
</div>
</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 */}
{getTokenLogo(token.symbol) ? (
<img
src={getTokenLogo(token.symbol)!}
alt={token.symbol}
className="w-12 h-12 rounded-full shadow-lg object-cover"
/>
) : (
<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}
/>
{/* XCM Teleport Modal */}
<XCMTeleportModal
isOpen={isXCMTeleportModalOpen}
onClose={() => setIsXCMTeleportModalOpen(false)}
/>
</div>
);
};