mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 11:18:01 +00:00
4f683538d3
Add full internationalization across 127+ components and pages. 790+ translation keys in en, tr, kmr, ckb, ar, fa locales. Remove duplicate keys and delete unused .json locale files.
1163 lines
47 KiB
TypeScript
1163 lines
47 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
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, Lock } 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 { LPStakeModal } from './LPStakeModal';
|
|
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 { t } = useTranslation();
|
|
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) : [];
|
|
});
|
|
const [isLPStakeModalOpen, setIsLPStakeModalOpen] = useState(false);
|
|
const [selectedLPForStake, setSelectedLPForStake] = useState<TokenBalance | null>(null);
|
|
|
|
// 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',
|
|
'HEZ-DOT 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)
|
|
// IMPORTANT: Assets are on Asset Hub, use assetHubApi (not relay chain api)
|
|
const fetchOtherTokens = async () => {
|
|
if (!assetHubApi || !isAssetHubReady || !selectedAccount) return;
|
|
|
|
try {
|
|
const tokens: TokenBalance[] = [];
|
|
|
|
// IMPORTANT: Only show custom tokens added by user
|
|
// Core tokens are shown in their own dedicated cards - exclude them here
|
|
// Using hardcoded IDs to avoid env variable issues
|
|
const excludedAssetIds = [
|
|
1, // PEZ
|
|
2, // wHEZ
|
|
3, // Old USDT (deprecated)
|
|
1000, // wUSDT (USDT)
|
|
];
|
|
const assetIdsToCheck = customTokenIds.filter((id) => !excludedAssetIds.includes(id));
|
|
|
|
for (const assetId of assetIdsToCheck) {
|
|
try {
|
|
// First check if asset exists on blockchain
|
|
const assetInfo = await assetHubApi.query.assets.asset(assetId);
|
|
if (!assetInfo || assetInfo.isNone) {
|
|
// Asset doesn't exist on blockchain - skip it
|
|
if (import.meta.env.DEV) console.log(`Asset ${assetId} not found on blockchain, skipping`);
|
|
continue;
|
|
}
|
|
|
|
// Asset exists - get metadata
|
|
const assetMetadata = await assetHubApi.query.assets.metadata(assetId);
|
|
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);
|
|
|
|
// Get balance (may be 0 if user hasn't received any)
|
|
let balanceFormatted = '0';
|
|
const assetBalance = await assetHubApi.query.assets.account(assetId, selectedAccount.address);
|
|
if (assetBalance.isSome) {
|
|
const assetData = assetBalance.unwrap();
|
|
const balance = assetData.balance.toString();
|
|
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
|
|
}
|
|
|
|
// Only show tokens that exist on blockchain
|
|
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,
|
|
});
|
|
}
|
|
}
|
|
|
|
// HEZ-DOT LP Token (ID: 2)
|
|
const hezDotLp = await assetHubApi.query.poolAssets.account(2, selectedAccount.address);
|
|
if (hezDotLp.isSome) {
|
|
const lpBalance = hezDotLp.unwrap().balance.toString();
|
|
const lpTokens = (parseInt(lpBalance) / divisor).toFixed(4);
|
|
if (parseFloat(lpTokens) > 0) {
|
|
lpTokensData.push({
|
|
assetId: 2,
|
|
symbol: 'HEZ-DOT LP',
|
|
name: 'HEZ-DOT 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(t('balance.tokenAlreadyAdded'));
|
|
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 with frontend fallback
|
|
const fetchAllScores = async () => {
|
|
if (!api || !isApiReady || !selectedAccount?.address) {
|
|
setScores({
|
|
trustScore: 0,
|
|
referralScore: 0,
|
|
stakingScore: 0,
|
|
tikiScore: 0,
|
|
totalScore: 0
|
|
});
|
|
return;
|
|
}
|
|
|
|
setLoadingScores(true);
|
|
try {
|
|
// Use fallback function: peopleApi for on-chain scores, api (Relay) for staking data
|
|
const userScores = await getAllScores(peopleApi || null, 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>{t('balance.connectWallet')}</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">
|
|
{t('balance.hezBalance')}
|
|
</CardTitle>
|
|
<div className="text-xs text-gray-500">{t('balance.multiChain')}</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" />
|
|
{t('balance.addFee')}
|
|
<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">
|
|
{t('balance.sendHezToTeyrcahins')}
|
|
</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
|
|
? t('balance.usdTotal', { amount: ((parseFloat(balance.total) + parseFloat(assetHubHezBalance) + parseFloat(peopleHezBalance)) * hezUsdPrice).toFixed(2) })
|
|
: t('balance.priceLoading')}
|
|
</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">{t('balance.relayChain')}</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">{t('balance.reserved', { amount: 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">{t('balance.assetHub')}</span>
|
|
<span className="text-xs text-gray-500">{t('balance.pezFees')}</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">{`⚠️ ${t('balance.lowForFees')}`}</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">{t('balance.peopleChain')}</span>
|
|
<span className="text-xs text-gray-500">{t('balance.identityFees')}</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">{`⚠️ ${t('balance.lowForFees')}`}</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">
|
|
{t('balance.pezBalance')}
|
|
</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" />
|
|
{t('balance.addFees')}
|
|
<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">
|
|
{t('balance.sendHezForPezFees')}
|
|
</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`
|
|
: t('balance.priceLoading')}
|
|
</div>
|
|
<div className="text-xs text-gray-500 mt-1">
|
|
{t('balance.govRewardsToken')}
|
|
</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">
|
|
{t('balance.usdtBalance')}
|
|
</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">
|
|
{t('balance.stablecoinOnAssetHub', { amount: usdtBalance })}
|
|
</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">
|
|
{t('balance.whezBalance')}
|
|
</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`
|
|
: t('balance.priceLoading')} • {t('balance.wrappedHezOnAssetHub')}
|
|
</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">
|
|
{t('balance.lpTokenPositions')}
|
|
</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 group">
|
|
<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="flex items-center gap-3">
|
|
<div className="text-right">
|
|
<div className="text-lg font-semibold text-white">{lp.balance}</div>
|
|
<div className="text-xs text-gray-500">{t('balance.poolShare')}</div>
|
|
</div>
|
|
<Button
|
|
size="sm"
|
|
onClick={() => {
|
|
setSelectedLPForStake(lp);
|
|
setIsLPStakeModalOpen(true);
|
|
}}
|
|
className="bg-gradient-to-r from-purple-600 to-pink-600 text-white font-medium hover:from-purple-500 hover:to-pink-500 border-0"
|
|
>
|
|
<Lock className="w-3 h-3 mr-1" />
|
|
{t('balance.stake')}
|
|
</Button>
|
|
</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">
|
|
{t('balance.accountInfo')}
|
|
</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">{t('balance.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">{t('balance.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">{t('balance.scoresFromBlockchain')}</div>
|
|
{loadingScores ? (
|
|
<div className="text-sm text-gray-400">{t('balance.loadingScores')}</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">{t('balance.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">{t('balance.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">{t('balance.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">{t('balance.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">{t('balance.totalScore')}</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">
|
|
{t('balance.otherAssets')}
|
|
</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" />
|
|
{t('balance.addToken')}
|
|
</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">{t('balance.noCustomTokens')}</p>
|
|
<p className="text-gray-600 text-xs mt-1">
|
|
{t('balance.addCustomTokensDesc')}
|
|
</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)}
|
|
/>
|
|
|
|
{/* LP Stake Modal */}
|
|
<LPStakeModal
|
|
isOpen={isLPStakeModalOpen}
|
|
onClose={() => {
|
|
setIsLPStakeModalOpen(false);
|
|
setSelectedLPForStake(null);
|
|
}}
|
|
lpToken={selectedLPForStake}
|
|
onStakeSuccess={() => fetchBalance()}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|