fix: DEX swap calculation and wallet refresh issues

- Fix AMM formula to use correct 3% LP fee (was 0.3%)
  - Runtime uses LPFee=30 (3% = 30/1000)
  - Changed to Uniswap V2 formula: amountOut = (amountIn * 970 * reserveOut) / (reserveIn * 1000 + amountIn * 970)
  - Fixes ProvidedMinimumNotSufficientForSwap error

- Fix wallet disconnection after successful swap
  - Added refreshBalances() to WalletContext
  - Replaced window.location.reload() with refreshBalances()
  - Wallet connection now persists after swap

Changes:
- src/components/TokenSwap.tsx: Correct AMM formula, async callback for refresh
- src/contexts/WalletContext.tsx: Add refreshBalances() export
This commit is contained in:
2025-11-01 08:18:24 +03:00
parent d780ad497d
commit b187105c18
2 changed files with 388 additions and 146 deletions
+292 -128
View File
@@ -7,11 +7,13 @@ import { Alert, AlertDescription } from '@/components/ui/alert';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { usePolkadot } from '@/contexts/PolkadotContext'; import { usePolkadot } from '@/contexts/PolkadotContext';
import { useWallet } from '@/contexts/WalletContext';
import { ASSET_IDS, formatBalance, parseAmount } from '@/lib/wallet'; import { ASSET_IDS, formatBalance, parseAmount } from '@/lib/wallet';
import { useToast } from '@/hooks/use-toast'; import { useToast } from '@/hooks/use-toast';
const TokenSwap = () => { const TokenSwap = () => {
const { api, isApiReady, selectedAccount } = usePolkadot(); const { api, isApiReady, selectedAccount } = usePolkadot();
const { balances, refreshBalances } = useWallet();
const { toast } = useToast(); const { toast } = useToast();
const [fromToken, setFromToken] = useState('PEZ'); const [fromToken, setFromToken] = useState('PEZ');
@@ -25,106 +27,207 @@ const TokenSwap = () => {
// DEX availability check // DEX availability check
const [isDexAvailable, setIsDexAvailable] = useState(false); const [isDexAvailable, setIsDexAvailable] = useState(false);
// Real balances from blockchain // Exchange rate and loading states
const [fromBalance, setFromBalance] = useState('0');
const [toBalance, setToBalance] = useState('0');
const [exchangeRate, setExchangeRate] = useState(0); const [exchangeRate, setExchangeRate] = useState(0);
const [isLoadingBalances, setIsLoadingBalances] = useState(false);
const [isLoadingRate, setIsLoadingRate] = useState(false); const [isLoadingRate, setIsLoadingRate] = useState(false);
// Get balances from wallet context
console.log('🔍 TokenSwap balances from context:', balances);
console.log('🔍 fromToken:', fromToken, 'toToken:', toToken);
const fromBalance = balances[fromToken as keyof typeof balances];
const toBalance = balances[toToken as keyof typeof balances];
console.log('🔍 Final balances:', { fromBalance, toBalance });
// Liquidity pool data // Liquidity pool data
const [liquidityPools, setLiquidityPools] = useState<any[]>([]); const [liquidityPools, setLiquidityPools] = useState<any[]>([]);
const [isLoadingPools, setIsLoadingPools] = useState(false); const [isLoadingPools, setIsLoadingPools] = useState(false);
const toAmount = fromAmount && exchangeRate > 0 // Pool reserves for AMM calculation
? (parseFloat(fromAmount) * exchangeRate).toFixed(4) const [poolReserves, setPoolReserves] = useState<{ reserve0: number; reserve1: number; asset0: number; asset1: number } | null>(null);
: '';
// Calculate toAmount using AMM constant product formula
const toAmount = React.useMemo(() => {
if (!fromAmount || !poolReserves || parseFloat(fromAmount) <= 0) return '';
const amountIn = parseFloat(fromAmount);
const { reserve0, reserve1, asset0, asset1 } = poolReserves;
// Determine which reserve is input and which is output
const fromAssetId = fromToken === 'HEZ' ? 0 : ASSET_IDS[fromToken as keyof typeof ASSET_IDS];
const isAsset0ToAsset1 = fromAssetId === asset0;
const reserveIn = isAsset0ToAsset1 ? reserve0 : reserve1;
const reserveOut = isAsset0ToAsset1 ? reserve1 : reserve0;
// Uniswap V2 AMM formula (matches Substrate runtime exactly)
// Runtime: amount_in_with_fee = amount_in * (1000 - LPFee) = amount_in * 970
// LPFee = 30 (3% fee, not 0.3%!)
// Formula: amountOut = (amountIn * 970 * reserveOut) / (reserveIn * 1000 + amountIn * 970)
const LP_FEE = 30; // 3% fee
const amountInWithFee = amountIn * (1000 - LP_FEE); // = amountIn * 970
const numerator = amountInWithFee * reserveOut;
const denominator = reserveIn * 1000 + amountInWithFee;
const amountOut = numerator / denominator;
console.log('🔍 Uniswap V2 AMM:', {
amountIn,
amountInWithFee,
reserveIn,
reserveOut,
numerator,
denominator,
amountOut,
feePercent: LP_FEE / 10 + '%'
});
return amountOut.toFixed(4);
}, [fromAmount, poolReserves, fromToken]);
// Check if AssetConversion pallet is available // Check if AssetConversion pallet is available
useEffect(() => { useEffect(() => {
console.log('🔍 Checking DEX availability...', { api: !!api, isApiReady });
if (api && isApiReady) { if (api && isApiReady) {
const hasAssetConversion = api.tx.assetConversion !== undefined; const hasAssetConversion = api.tx.assetConversion !== undefined;
console.log('🔍 AssetConversion pallet check:', hasAssetConversion);
setIsDexAvailable(hasAssetConversion); setIsDexAvailable(hasAssetConversion);
if (!hasAssetConversion) { if (!hasAssetConversion) {
console.warn('AssetConversion pallet not available in runtime'); console.warn('⚠️ AssetConversion pallet not available in runtime');
} else {
console.log('✅ AssetConversion pallet is available!');
} }
} }
}, [api, isApiReady]); }, [api, isApiReady]);
// Fetch balances from blockchain
useEffect(() => {
const fetchBalances = async () => {
if (!api || !isApiReady || !selectedAccount) {
return;
}
setIsLoadingBalances(true);
try {
const fromAssetId = ASSET_IDS[fromToken as keyof typeof ASSET_IDS];
const toAssetId = ASSET_IDS[toToken as keyof typeof ASSET_IDS];
// Fetch balances from Assets pallet
const [fromAssetBalance, toAssetBalance] = await Promise.all([
api.query.assets.account(fromAssetId, selectedAccount.address),
api.query.assets.account(toAssetId, selectedAccount.address),
]);
// Format balances (12 decimals for PEZ/HEZ tokens)
const fromBal = fromAssetBalance.toJSON() as any;
const toBal = toAssetBalance.toJSON() as any;
setFromBalance(fromBal ? formatBalance(fromBal.balance.toString(), 12) : '0');
setToBalance(toBal ? formatBalance(toBal.balance.toString(), 12) : '0');
} catch (error) {
console.error('Failed to fetch balances:', error);
toast({
title: 'Error',
description: 'Failed to fetch token balances',
variant: 'destructive',
});
} finally {
setIsLoadingBalances(false);
}
};
fetchBalances();
}, [api, isApiReady, selectedAccount, fromToken, toToken, toast]);
// Fetch exchange rate from AssetConversion pool // Fetch exchange rate from AssetConversion pool
// Always use wHEZ/PEZ pool (the only valid pool)
useEffect(() => { useEffect(() => {
const fetchExchangeRate = async () => { const fetchExchangeRate = async () => {
console.log('🔍 fetchExchangeRate check:', { api: !!api, isApiReady, isDexAvailable, fromToken, toToken });
if (!api || !isApiReady || !isDexAvailable) { if (!api || !isApiReady || !isDexAvailable) {
console.log('⚠️ Skipping fetchExchangeRate:', { api: !!api, isApiReady, isDexAvailable });
return; return;
} }
console.log('✅ Starting fetchExchangeRate...');
setIsLoadingRate(true); setIsLoadingRate(true);
try { try {
const fromAssetId = ASSET_IDS[fromToken as keyof typeof ASSET_IDS]; // Map user-selected tokens to actual pool assets
const toAssetId = ASSET_IDS[toToken as keyof typeof ASSET_IDS]; // HEZ → wHEZ (Asset 0) behind the scenes
const getPoolAssetId = (token: string) => {
if (token === 'HEZ') return 0; // wHEZ
return ASSET_IDS[token as keyof typeof ASSET_IDS];
};
// Create pool asset tuple [asset1, asset2] const fromAssetId = getPoolAssetId(fromToken);
const toAssetId = getPoolAssetId(toToken);
console.log('🔍 Looking for pool:', { fromToken, toToken, fromAssetId, toAssetId });
// IMPORTANT: Pool ID must be sorted (smaller asset ID first)
const [asset1, asset2] = fromAssetId < toAssetId
? [fromAssetId, toAssetId]
: [toAssetId, fromAssetId];
console.log('🔍 Sorted pool assets:', { asset1, asset2 });
// Create pool asset tuple [asset1, asset2] - must be sorted!
const poolAssets = [ const poolAssets = [
{ NativeOrAsset: { Asset: fromAssetId } }, { NativeOrAsset: { Asset: asset1 } },
{ NativeOrAsset: { Asset: toAssetId } } { NativeOrAsset: { Asset: asset2 } }
]; ];
console.log('🔍 Pool query with:', poolAssets);
// Query pool from AssetConversion pallet // Query pool from AssetConversion pallet
const poolInfo = await api.query.assetConversion.pools(poolAssets); const poolInfo = await api.query.assetConversion.pools(poolAssets);
console.log('🔍 Pool query result:', poolInfo.toHuman());
console.log('🔍 Pool isEmpty?', poolInfo.isEmpty, 'exists?', !poolInfo.isEmpty);
if (poolInfo && !poolInfo.isEmpty) { if (poolInfo && !poolInfo.isEmpty) {
const pool = poolInfo.toJSON() as any; const pool = poolInfo.toJSON() as any;
console.log('🔍 Pool data:', pool);
if (pool && pool[0] && pool[1]) { try {
// Pool structure: [reserve0, reserve1] // New pallet version: reserves are stored in pool account balances
const reserve0 = parseFloat(pool[0].toString()); // AccountIdConverter implementation in substrate:
const reserve1 = parseFloat(pool[1].toString()); // blake2_256(&Encode::encode(&(PalletId, PoolId))[..])
console.log('🔍 Deriving pool account using AccountIdConverter...');
const { stringToU8a } = await import('@polkadot/util');
const { blake2AsU8a } = await import('@polkadot/util-crypto');
// Calculate exchange rate // PalletId for AssetConversion: "py/ascon" (8 bytes)
const rate = reserve1 / reserve0; const PALLET_ID = stringToU8a('py/ascon');
setExchangeRate(rate);
} else { // Create PoolId tuple (u32, u32)
console.warn('Pool has no reserves'); const poolId = api.createType('(u32, u32)', [asset1, asset2]);
console.log('🔍 Pool ID:', poolId.toHuman());
// Create (PalletId, PoolId) tuple: ([u8; 8], (u32, u32))
const palletIdType = api.createType('[u8; 8]', PALLET_ID);
const fullTuple = api.createType('([u8; 8], (u32, u32))', [palletIdType, poolId]);
console.log('🔍 Full tuple encoded length:', fullTuple.toU8a().length);
console.log('🔍 Full tuple bytes:', Array.from(fullTuple.toU8a()));
// Hash the SCALE-encoded tuple
const accountHash = blake2AsU8a(fullTuple.toU8a(), 256);
console.log('🔍 Account hash:', Array.from(accountHash).slice(0, 8));
const poolAccountId = api.createType('AccountId32', accountHash);
console.log('🔍 Pool AccountId (NEW METHOD):', poolAccountId.toString());
// Query pool account's asset balances
console.log('🔍 Querying reserves for asset', asset1, 'and', asset2);
const reserve0Query = await api.query.assets.account(asset1, poolAccountId);
const reserve1Query = await api.query.assets.account(asset2, poolAccountId);
console.log('🔍 Reserve0 query result:', reserve0Query.toHuman());
console.log('🔍 Reserve1 query result:', reserve1Query.toHuman());
console.log('🔍 Reserve0 isEmpty?', reserve0Query.isEmpty);
console.log('🔍 Reserve1 isEmpty?', reserve1Query.isEmpty);
const reserve0Data = reserve0Query.toJSON() as any;
const reserve1Data = reserve1Query.toJSON() as any;
console.log('🔍 Reserve0 JSON:', reserve0Data);
console.log('🔍 Reserve1 JSON:', reserve1Data);
if (reserve0Data && reserve1Data && reserve0Data.balance && reserve1Data.balance) {
// Parse hex string balances to BigInt, then to number
const balance0Hex = reserve0Data.balance.toString();
const balance1Hex = reserve1Data.balance.toString();
console.log('🔍 Raw hex balances:', { balance0Hex, balance1Hex });
const reserve0 = Number(BigInt(balance0Hex)) / 1e12;
const reserve1 = Number(BigInt(balance1Hex)) / 1e12;
console.log('✅ Reserves found:', { reserve0, reserve1 });
// Store pool reserves for AMM calculation
setPoolReserves({
reserve0,
reserve1,
asset0: asset1, // Sorted pool always has asset1 < asset2
asset1: asset2
});
// Also calculate simple exchange rate for display
const rate = fromAssetId === asset1
? reserve1 / reserve0 // from asset1 to asset2
: reserve0 / reserve1; // from asset2 to asset1
console.log('✅ Exchange rate:', rate, 'direction:', fromAssetId === asset1 ? 'asset1→asset2' : 'asset2→asset1');
setExchangeRate(rate);
} else {
console.warn('⚠️ Pool has no reserves - reserve0Data:', reserve0Data, 'reserve1Data:', reserve1Data);
setExchangeRate(0);
}
} catch (err) {
console.error('❌ Error deriving pool account:', err);
setExchangeRate(0); setExchangeRate(0);
} }
} else { } else {
@@ -228,67 +331,128 @@ const TokenSwap = () => {
setIsSwapping(true); setIsSwapping(true);
try { try {
const fromAssetId = ASSET_IDS[fromToken as keyof typeof ASSET_IDS];
const toAssetId = ASSET_IDS[toToken as keyof typeof ASSET_IDS];
const amountIn = parseAmount(fromAmount, 12); const amountIn = parseAmount(fromAmount, 12);
// Calculate minimum amount out based on slippage
const minAmountOut = parseAmount( const minAmountOut = parseAmount(
(parseFloat(toAmount) * (1 - parseFloat(slippage) / 100)).toString(), (parseFloat(toAmount) * (1 - parseFloat(slippage) / 100)).toString(),
12 12
); );
// Create path for swap
const path = [
{ NativeOrAsset: { Asset: fromAssetId } },
{ NativeOrAsset: { Asset: toAssetId } }
];
// Get signer from extension // Get signer from extension
const { web3FromAddress } = await import('@polkadot/extension-dapp'); const { web3FromAddress } = await import('@polkadot/extension-dapp');
const injector = await web3FromAddress(selectedAccount.address); const injector = await web3FromAddress(selectedAccount.address);
// Submit swap transaction to AssetConversion pallet // Build transaction based on token types
const tx = api.tx.assetConversion.swapExactTokensForTokens( let tx;
path,
amountIn.toString(),
minAmountOut.toString(),
selectedAccount.address,
true // keep_alive
);
if (fromToken === 'HEZ' && toToken === 'PEZ') {
// HEZ → PEZ: wrap(HEZ→wHEZ) then swap(wHEZ→PEZ)
const wrapTx = api.tx.tokenWrapper.wrap(amountIn.toString());
const swapPath = [
{ NativeOrAsset: { Asset: 0 } }, // wHEZ
{ NativeOrAsset: { Asset: 1 } } // PEZ
];
const swapTx = api.tx.assetConversion.swapExactTokensForTokens(
swapPath,
amountIn.toString(),
minAmountOut.toString(),
selectedAccount.address,
true
);
tx = api.tx.utility.batchAll([wrapTx, swapTx]);
} else if (fromToken === 'PEZ' && toToken === 'HEZ') {
// PEZ → HEZ: swap(PEZ→wHEZ) then unwrap(wHEZ→HEZ)
const swapPath = [
{ NativeOrAsset: { Asset: 1 } }, // PEZ
{ NativeOrAsset: { Asset: 0 } } // 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 (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];
};
const swapPath = [
{ NativeOrAsset: { Asset: getPoolAssetId(fromToken) } },
{ NativeOrAsset: { Asset: getPoolAssetId(toToken) } }
];
tx = api.tx.assetConversion.swapExactTokensForTokens(
swapPath,
amountIn.toString(),
minAmountOut.toString(),
selectedAccount.address,
true
);
}
// Sign and send transaction
await tx.signAndSend( await tx.signAndSend(
selectedAccount.address, selectedAccount.address,
{ signer: injector.signer }, { signer: injector.signer },
({ status, events }) => { async ({ status, events, dispatchError }) => {
console.log('🔍 Transaction status:', status.toHuman());
if (status.isInBlock) { if (status.isInBlock) {
console.log('Swap in block:', status.asInBlock.toHex()); console.log('✅ Transaction in block:', status.asInBlock.toHex());
toast({ toast({
title: 'Transaction Submitted', title: 'Transaction Submitted',
description: `Swap in block ${status.asInBlock.toHex().slice(0, 10)}...`, description: `Processing in block ${status.asInBlock.toHex().slice(0, 10)}...`,
}); });
} }
if (status.isFinalized) { if (status.isFinalized) {
console.log('Swap finalized:', status.asFinalized.toHex()); console.log('✅ Transaction finalized:', status.asFinalized.toHex());
console.log('🔍 All events:', events.map(({ event }) => event.toHuman()));
console.log('🔍 dispatchError:', dispatchError?.toHuman());
// Check for successful swap event // Check for errors
const swapEvent = events.find(({ event }) => if (dispatchError) {
let errorMessage = 'Transaction failed';
if (dispatchError.isModule) {
const decoded = api.registry.findMetaError(dispatchError.asModule);
errorMessage = `${decoded.section}.${decoded.name}: ${decoded.docs}`;
}
toast({
title: 'Error',
description: errorMessage,
variant: 'destructive',
});
setIsSwapping(false);
return;
}
// Success - check for swap event
const hasSwapEvent = events.some(({ event }) =>
api.events.assetConversion?.SwapExecuted?.is(event) api.events.assetConversion?.SwapExecuted?.is(event)
); );
if (swapEvent) { if (hasSwapEvent || fromToken === 'HEZ' || toToken === 'HEZ') {
toast({ toast({
title: 'Success!', title: 'Success!',
description: `Swapped ${fromAmount} ${fromToken} for ${toAmount} ${toToken}`, description: `Swapped ${fromAmount} ${fromToken} for ~${toAmount} ${toToken}`,
}); });
setShowConfirm(false); setShowConfirm(false);
setFromAmount(''); setFromAmount('');
// Refresh balances // Refresh balances without page reload
window.location.reload(); await refreshBalances();
console.log('✅ Balances refreshed after swap');
} else { } else {
toast({ toast({
title: 'Error', title: 'Error',
@@ -326,7 +490,7 @@ const TokenSwap = () => {
<div> <div>
<h2 className="text-2xl font-bold mb-2">DEX Coming Soon</h2> <h2 className="text-2xl font-bold mb-2">DEX Coming Soon</h2>
<p className="text-gray-400 max-w-md mx-auto"> <p className="text-gray-300 max-w-md mx-auto">
The AssetConversion pallet is not yet enabled in the runtime. The AssetConversion pallet is not yet enabled in the runtime.
Token swapping functionality will be available after the next runtime upgrade. Token swapping functionality will be available after the next runtime upgrade.
</p> </p>
@@ -368,11 +532,11 @@ const TokenSwap = () => {
)} )}
<div className="space-y-4"> <div className="space-y-4">
<div className="bg-gray-50 rounded-lg p-4 text-gray-900"> <div className="bg-gray-800 border border-gray-700 rounded-lg p-4">
<div className="flex justify-between mb-2"> <div className="flex justify-between mb-2">
<span className="text-sm text-gray-900">From</span> <span className="text-sm text-gray-400">From</span>
<span className="text-sm text-gray-900"> <span className="text-sm text-gray-400">
Balance: {isLoadingBalances ? '...' : fromBalance} {fromToken} Balance: {fromBalance} {fromToken}
</span> </span>
</div> </div>
<div className="flex gap-3"> <div className="flex gap-3">
@@ -381,32 +545,32 @@ const TokenSwap = () => {
value={fromAmount} value={fromAmount}
onChange={(e) => setFromAmount(e.target.value)} onChange={(e) => setFromAmount(e.target.value)}
placeholder="0.0" placeholder="0.0"
className="text-2xl font-bold border-0 bg-transparent" className="text-2xl font-bold border-0 bg-transparent text-white placeholder:text-gray-600"
disabled={!selectedAccount} disabled={!selectedAccount}
/> />
<Button variant="outline" className="min-w-[100px]"> <Button variant="outline" className="min-w-[100px] border-gray-600 hover:border-gray-500">
{fromToken === 'PEZ' ? '🟣 PEZ' : '🟡 HEZ'} {fromToken === 'PEZ' ? '🟣 PEZ' : '🟡 HEZ'}
</Button> </Button>
</div> </div>
</div> </div>
<div className="flex justify-center"> <div className="flex justify-center -my-2">
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={handleSwap} onClick={handleSwap}
className="rounded-full bg-white border-2" className="rounded-full bg-gray-800 border-2 border-gray-700 hover:bg-gray-700 hover:border-gray-600"
disabled={!selectedAccount} disabled={!selectedAccount}
> >
<ArrowDownUp className="h-5 w-5" /> <ArrowDownUp className="h-5 w-5 text-gray-300" />
</Button> </Button>
</div> </div>
<div className="bg-gray-50 rounded-lg p-4 text-gray-900"> <div className="bg-gray-800 border border-gray-700 rounded-lg p-4">
<div className="flex justify-between mb-2"> <div className="flex justify-between mb-2">
<span className="text-sm text-gray-900">To</span> <span className="text-sm text-gray-400">To</span>
<span className="text-sm text-gray-900"> <span className="text-sm text-gray-400">
Balance: {isLoadingBalances ? '...' : toBalance} {toToken} Balance: {toBalance} {toToken}
</span> </span>
</div> </div>
<div className="flex gap-3"> <div className="flex gap-3">
@@ -415,18 +579,18 @@ const TokenSwap = () => {
value={toAmount} value={toAmount}
readOnly readOnly
placeholder="0.0" placeholder="0.0"
className="text-2xl font-bold border-0 bg-transparent" className="text-2xl font-bold border-0 bg-transparent text-white placeholder:text-gray-600"
/> />
<Button variant="outline" className="min-w-[100px]"> <Button variant="outline" className="min-w-[100px] border-gray-600 hover:border-gray-500">
{toToken === 'PEZ' ? '🟣 PEZ' : '🟡 HEZ'} {toToken === 'PEZ' ? '🟣 PEZ' : '🟡 HEZ'}
</Button> </Button>
</div> </div>
</div> </div>
<div className="bg-blue-50 rounded-lg p-3 text-gray-900"> <div className="bg-blue-900/20 border border-blue-800/30 rounded-lg p-3">
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-gray-900">Exchange Rate</span> <span className="text-gray-300">Exchange Rate</span>
<span className="font-semibold text-gray-900"> <span className="font-semibold text-blue-400">
{isLoadingRate ? ( {isLoadingRate ? (
'Loading...' 'Loading...'
) : exchangeRate > 0 ? ( ) : exchangeRate > 0 ? (
@@ -437,8 +601,8 @@ const TokenSwap = () => {
</span> </span>
</div> </div>
<div className="flex justify-between text-sm mt-1"> <div className="flex justify-between text-sm mt-1">
<span className="text-gray-900">Slippage Tolerance</span> <span className="text-gray-300">Slippage Tolerance</span>
<span className="font-semibold text-gray-900">{slippage}%</span> <span className="font-semibold text-blue-400">{slippage}%</span>
</div> </div>
</div> </div>
@@ -459,24 +623,24 @@ const TokenSwap = () => {
</h3> </h3>
{isLoadingPools ? ( {isLoadingPools ? (
<div className="text-center text-gray-500 py-8">Loading pools...</div> <div className="text-center text-gray-400 py-8">Loading pools...</div>
) : liquidityPools.length > 0 ? ( ) : liquidityPools.length > 0 ? (
<div className="space-y-3"> <div className="space-y-3">
{liquidityPools.map((pool, idx) => ( {liquidityPools.map((pool, idx) => (
<div key={idx} className="flex justify-between items-center p-3 bg-gray-50 rounded-lg text-gray-900"> <div key={idx} className="flex justify-between items-center p-3 bg-gray-800 border border-gray-700 rounded-lg hover:border-gray-600 transition-colors">
<div> <div>
<div className="font-semibold text-gray-900">{pool.pool}</div> <div className="font-semibold text-gray-200">{pool.pool}</div>
<div className="text-sm text-gray-900">TVL: {pool.tvl}</div> <div className="text-sm text-gray-400">TVL: {pool.tvl}</div>
</div> </div>
<div className="text-right"> <div className="text-right">
<div className="text-green-600 font-semibold">{pool.apr} APR</div> <div className="text-green-400 font-semibold">{pool.apr} APR</div>
<div className="text-sm text-gray-900">Vol: {pool.volume}</div> <div className="text-sm text-gray-400">Vol: {pool.volume}</div>
</div> </div>
</div> </div>
))} ))}
</div> </div>
) : ( ) : (
<div className="text-center text-gray-500 py-8"> <div className="text-center text-gray-400 py-8">
No liquidity pools available yet No liquidity pools available yet
</div> </div>
)} )}
@@ -490,7 +654,7 @@ const TokenSwap = () => {
Recent Swaps Recent Swaps
</h3> </h3>
<div className="text-center text-gray-500 py-8"> <div className="text-center text-gray-400 py-8">
{selectedAccount ? 'No swap history yet' : 'Connect wallet to view history'} {selectedAccount ? 'No swap history yet' : 'Connect wallet to view history'}
</div> </div>
</Card> </Card>
@@ -533,22 +697,22 @@ const TokenSwap = () => {
<DialogTitle>Confirm Swap</DialogTitle> <DialogTitle>Confirm Swap</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="space-y-4"> <div className="space-y-4">
<div className="p-4 bg-gray-50 rounded-lg text-gray-900"> <div className="p-4 bg-gray-800 border border-gray-700 rounded-lg">
<div className="flex justify-between mb-2"> <div className="flex justify-between mb-2">
<span className="text-gray-900">You Pay</span> <span className="text-gray-300">You Pay</span>
<span className="font-bold text-gray-900">{fromAmount} {fromToken}</span> <span className="font-bold text-white">{fromAmount} {fromToken}</span>
</div> </div>
<div className="flex justify-between mb-2"> <div className="flex justify-between mb-2">
<span className="text-gray-900">You Receive</span> <span className="text-gray-300">You Receive</span>
<span className="font-bold text-gray-900">{toAmount} {toToken}</span> <span className="font-bold text-white">{toAmount} {toToken}</span>
</div> </div>
<div className="flex justify-between mt-3 pt-3 border-t text-sm"> <div className="flex justify-between mt-3 pt-3 border-t border-gray-700 text-sm">
<span className="text-gray-600">Exchange Rate</span> <span className="text-gray-400">Exchange Rate</span>
<span className="text-gray-600">1 {fromToken} = {exchangeRate.toFixed(4)} {toToken}</span> <span className="text-gray-400">1 {fromToken} = {exchangeRate.toFixed(4)} {toToken}</span>
</div> </div>
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-gray-600">Slippage</span> <span className="text-gray-400">Slippage</span>
<span className="text-gray-600">{slippage}%</span> <span className="text-gray-400">{slippage}%</span>
</div> </div>
</div> </div>
<Button <Button
+87 -9
View File
@@ -9,27 +9,44 @@ import { usePolkadot } from './PolkadotContext';
import { WALLET_ERRORS, formatBalance, ASSET_IDS } from '@/lib/wallet'; import { WALLET_ERRORS, formatBalance, ASSET_IDS } from '@/lib/wallet';
import type { InjectedAccountWithMeta } from '@polkadot/extension-inject/types'; import type { InjectedAccountWithMeta } from '@polkadot/extension-inject/types';
interface TokenBalances {
HEZ: string;
PEZ: string;
wHEZ: string;
}
interface WalletContextType { interface WalletContextType {
isConnected: boolean; isConnected: boolean;
account: string | null; // Current selected account address account: string | null; // Current selected account address
accounts: InjectedAccountWithMeta[]; accounts: InjectedAccountWithMeta[];
balance: string; balance: string; // Legacy: HEZ balance
balances: TokenBalances; // All token balances
error: string | null; error: string | null;
connectWallet: () => Promise<void>; connectWallet: () => Promise<void>;
disconnect: () => void; disconnect: () => void;
switchAccount: (account: InjectedAccountWithMeta) => void; switchAccount: (account: InjectedAccountWithMeta) => void;
signTransaction: (tx: any) => Promise<string>; signTransaction: (tx: any) => Promise<string>;
signMessage: (message: string) => Promise<string>; signMessage: (message: string) => Promise<string>;
refreshBalances: () => Promise<void>; // Refresh all token balances
} }
const WalletContext = createContext<WalletContextType | undefined>(undefined); const WalletContext = createContext<WalletContextType | undefined>(undefined);
export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const polkadot = usePolkadot(); const polkadot = usePolkadot();
console.log('🎯 WalletProvider render:', {
hasApi: !!polkadot.api,
isApiReady: polkadot.isApiReady,
selectedAccount: polkadot.selectedAccount?.address,
accountsCount: polkadot.accounts.length
});
const [balance, setBalance] = useState<string>('0'); const [balance, setBalance] = useState<string>('0');
const [balances, setBalances] = useState<TokenBalances>({ HEZ: '0', PEZ: '0', wHEZ: '0' });
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
// Fetch balance when account changes // Fetch all token balances when account changes
const updateBalance = useCallback(async (address: string) => { const updateBalance = useCallback(async (address: string) => {
if (!polkadot.api || !polkadot.isApiReady) { if (!polkadot.api || !polkadot.isApiReady) {
console.warn('API not ready, cannot fetch balance'); console.warn('API not ready, cannot fetch balance');
@@ -37,13 +54,59 @@ export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ childr
} }
try { try {
// Query native token balance (PEZ) console.log('💰 Fetching all token balances for:', address);
const { data: balance } = await polkadot.api.query.system.account(address);
const formattedBalance = formatBalance(balance.free.toString()); // Fetch HEZ (native token)
setBalance(formattedBalance); const { data: nativeBalance } = await polkadot.api.query.system.account(address);
const hezBalance = formatBalance(nativeBalance.free.toString());
setBalance(hezBalance); // Legacy support
// Fetch PEZ (Asset ID: 1)
let pezBalance = '0';
try {
const pezData = await polkadot.api.query.assets.account(ASSET_IDS.PEZ, address);
console.log('📊 Raw PEZ data:', pezData.toHuman());
if (pezData.isSome) {
const assetData = pezData.unwrap();
const pezAmount = assetData.balance.toString();
pezBalance = formatBalance(pezAmount);
console.log('✅ PEZ balance found:', pezBalance);
} else {
console.warn('⚠️ PEZ asset not found for this account');
}
} catch (err) {
console.error('❌ Failed to fetch PEZ balance:', err);
}
// Fetch wHEZ (Asset ID: 0)
let whezBalance = '0';
try {
const whezData = await polkadot.api.query.assets.account(ASSET_IDS.WHEZ, address);
console.log('📊 Raw wHEZ data:', whezData.toHuman());
if (whezData.isSome) {
const assetData = whezData.unwrap();
const whezAmount = assetData.balance.toString();
whezBalance = formatBalance(whezAmount);
console.log('✅ wHEZ balance found:', whezBalance);
} else {
console.warn('⚠️ wHEZ asset not found for this account');
}
} catch (err) {
console.error('❌ Failed to fetch wHEZ balance:', err);
}
setBalances({
HEZ: hezBalance,
PEZ: pezBalance,
wHEZ: whezBalance,
});
console.log('✅ Balances updated:', { HEZ: hezBalance, PEZ: pezBalance, wHEZ: whezBalance });
} catch (err) { } catch (err) {
console.error('Failed to fetch balance:', err); console.error('Failed to fetch balances:', err);
setError('Failed to fetch balance'); setError('Failed to fetch balances');
} }
}, [polkadot.api, polkadot.isApiReady]); }, [polkadot.api, polkadot.isApiReady]);
@@ -122,10 +185,16 @@ export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ childr
// Update balance when selected account changes // Update balance when selected account changes
useEffect(() => { useEffect(() => {
console.log('🔄 WalletContext useEffect triggered!', {
hasAccount: !!polkadot.selectedAccount,
isApiReady: polkadot.isApiReady,
address: polkadot.selectedAccount?.address
});
if (polkadot.selectedAccount && polkadot.isApiReady) { if (polkadot.selectedAccount && polkadot.isApiReady) {
updateBalance(polkadot.selectedAccount.address); updateBalance(polkadot.selectedAccount.address);
} }
}, [polkadot.selectedAccount, polkadot.isApiReady, updateBalance]); }, [polkadot.selectedAccount, polkadot.isApiReady]);
// Sync error state with PolkadotContext // Sync error state with PolkadotContext
useEffect(() => { useEffect(() => {
@@ -134,17 +203,26 @@ export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ childr
} }
}, [polkadot.error]); }, [polkadot.error]);
// Refresh balances for current account
const refreshBalances = useCallback(async () => {
if (polkadot.selectedAccount) {
await updateBalance(polkadot.selectedAccount.address);
}
}, [polkadot.selectedAccount, updateBalance]);
const value: WalletContextType = { const value: WalletContextType = {
isConnected: polkadot.accounts.length > 0, isConnected: polkadot.accounts.length > 0,
account: polkadot.selectedAccount?.address || null, account: polkadot.selectedAccount?.address || null,
accounts: polkadot.accounts, accounts: polkadot.accounts,
balance, balance,
balances,
error: error || polkadot.error, error: error || polkadot.error,
connectWallet, connectWallet,
disconnect, disconnect,
switchAccount, switchAccount,
signTransaction, signTransaction,
signMessage, signMessage,
refreshBalances,
}; };
return ( return (