mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-06-09 20:11:02 +00:00
fix: TokenSwap to use native HEZ with XCM Location format
- Changed HEZ from wHEZ (asset 0) to native token (NATIVE_TOKEN_ID = -1) - Updated pool query to use XCM MultiLocation format - Use runtime API quotePriceExactTokensForTokens for exchange rate - Updated swap transactions to use XCM Location paths - Added multi-hop support for PEZ ↔ USDT through native HEZ
This commit is contained in:
+107
-108
@@ -10,6 +10,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
|||||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||||
import { useWallet } from '@/contexts/WalletContext';
|
import { useWallet } from '@/contexts/WalletContext';
|
||||||
import { ASSET_IDS, formatBalance, parseAmount } from '@pezkuwi/lib/wallet';
|
import { ASSET_IDS, formatBalance, parseAmount } from '@pezkuwi/lib/wallet';
|
||||||
|
import { formatAssetLocation, NATIVE_TOKEN_ID } from '@pezkuwi/utils/dex';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
import { KurdistanSun } from './KurdistanSun';
|
import { KurdistanSun } from './KurdistanSun';
|
||||||
import { PriceChart } from './trading/PriceChart';
|
import { PriceChart } from './trading/PriceChart';
|
||||||
@@ -98,7 +99,8 @@ const TokenSwap = () => {
|
|||||||
const { reserve0, reserve1, asset0 } = poolReserves;
|
const { reserve0, reserve1, asset0 } = poolReserves;
|
||||||
|
|
||||||
// Determine which reserve is input and which is output
|
// Determine which reserve is input and which is output
|
||||||
const fromAssetId = fromToken === 'HEZ' ? 0 : ASSET_IDS[fromToken as keyof typeof ASSET_IDS];
|
// Native HEZ uses NATIVE_TOKEN_ID (-1)
|
||||||
|
const fromAssetId = fromToken === 'HEZ' ? NATIVE_TOKEN_ID : ASSET_IDS[fromToken as keyof typeof ASSET_IDS];
|
||||||
const isAsset0ToAsset1 = fromAssetId === asset0;
|
const isAsset0ToAsset1 = fromAssetId === asset0;
|
||||||
|
|
||||||
const reserveIn = isAsset0ToAsset1 ? reserve0 : reserve1;
|
const reserveIn = isAsset0ToAsset1 ? reserve0 : reserve1;
|
||||||
@@ -179,9 +181,9 @@ const TokenSwap = () => {
|
|||||||
setIsLoadingRate(true);
|
setIsLoadingRate(true);
|
||||||
try {
|
try {
|
||||||
// Map user-selected tokens to actual pool assets
|
// Map user-selected tokens to actual pool assets
|
||||||
// HEZ → wHEZ (Asset 0) behind the scenes
|
// HEZ → Native token (NATIVE_TOKEN_ID = -1)
|
||||||
const getPoolAssetId = (token: string) => {
|
const getPoolAssetId = (token: string) => {
|
||||||
if (token === 'HEZ') return 0; // wHEZ
|
if (token === 'HEZ') return NATIVE_TOKEN_ID; // Native HEZ (-1)
|
||||||
if (token === 'PEZ') return 1;
|
if (token === 'PEZ') return 1;
|
||||||
if (token === 'USDT') return 1000;
|
if (token === 'USDT') return 1000;
|
||||||
return ASSET_IDS[token as keyof typeof ASSET_IDS];
|
return ASSET_IDS[token as keyof typeof ASSET_IDS];
|
||||||
@@ -192,20 +194,27 @@ const TokenSwap = () => {
|
|||||||
|
|
||||||
if (import.meta.env.DEV) console.log('🔍 Looking for pool:', { fromToken, toToken, fromAssetId, toAssetId });
|
if (import.meta.env.DEV) console.log('🔍 Looking for pool:', { fromToken, toToken, fromAssetId, toAssetId });
|
||||||
|
|
||||||
// IMPORTANT: Pool ID must be sorted (smaller asset ID first)
|
// IMPORTANT: Pool ID must be sorted (native token first, then by asset ID)
|
||||||
const [asset1, asset2] = fromAssetId < toAssetId
|
// Native token (-1) always comes first
|
||||||
|
const [asset1, asset2] = fromAssetId === NATIVE_TOKEN_ID
|
||||||
? [fromAssetId, toAssetId]
|
? [fromAssetId, toAssetId]
|
||||||
: [toAssetId, fromAssetId];
|
: toAssetId === NATIVE_TOKEN_ID
|
||||||
|
? [toAssetId, fromAssetId]
|
||||||
|
: fromAssetId < toAssetId
|
||||||
|
? [fromAssetId, toAssetId]
|
||||||
|
: [toAssetId, fromAssetId];
|
||||||
|
|
||||||
if (import.meta.env.DEV) console.log('🔍 Sorted pool assets:', { asset1, asset2 });
|
if (import.meta.env.DEV) console.log('🔍 Sorted pool assets:', { asset1, asset2 });
|
||||||
|
|
||||||
// Create pool asset tuple [asset1, asset2] - must be sorted!
|
// Create pool asset tuple using XCM Location format
|
||||||
|
// Native token: { parents: 1, interior: 'Here' }
|
||||||
|
// Assets: { parents: 0, interior: { X2: [{ PalletInstance: 50 }, { GeneralIndex: id }] } }
|
||||||
const poolAssets = [
|
const poolAssets = [
|
||||||
{ NativeOrAsset: { Asset: asset1 } },
|
formatAssetLocation(asset1),
|
||||||
{ NativeOrAsset: { Asset: asset2 } }
|
formatAssetLocation(asset2)
|
||||||
];
|
];
|
||||||
|
|
||||||
if (import.meta.env.DEV) console.log('🔍 Pool query with:', poolAssets);
|
if (import.meta.env.DEV) console.log('🔍 Pool query with XCM Locations:', poolAssets);
|
||||||
|
|
||||||
// Query pool from AssetConversion pallet
|
// Query pool from AssetConversion pallet
|
||||||
const poolInfo = await assetHubApi.query.assetConversion.pools(poolAssets);
|
const poolInfo = await assetHubApi.query.assetConversion.pools(poolAssets);
|
||||||
@@ -218,90 +227,68 @@ const TokenSwap = () => {
|
|||||||
if (import.meta.env.DEV) console.log('🔍 Pool data:', pool);
|
if (import.meta.env.DEV) console.log('🔍 Pool data:', pool);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// New pallet version: reserves are stored in pool account balances
|
// Use Runtime API to get exchange rate (quotePriceExactTokensForTokens)
|
||||||
// AccountIdConverter implementation in substrate:
|
// This is more reliable than deriving pool account manually
|
||||||
// blake2_256(&Encode::encode(&(PalletId, PoolId))[..])
|
const decimals0 = asset1 === 1000 ? 6 : 12; // wUSDT: 6, others: 12
|
||||||
if (import.meta.env.DEV) console.log('🔍 Deriving pool account using AccountIdConverter...');
|
const decimals1 = asset2 === 1000 ? 6 : 12;
|
||||||
const { stringToU8a } = await import('@pezkuwi/util');
|
|
||||||
const { blake2AsU8a } = await import('@pezkuwi/util-crypto');
|
|
||||||
|
|
||||||
// PalletId for AssetConversion: "py/ascon" (8 bytes)
|
const oneUnit = BigInt(Math.pow(10, decimals0));
|
||||||
const PALLET_ID = stringToU8a('py/ascon');
|
|
||||||
|
|
||||||
// Create PoolId tuple (u32, u32)
|
if (import.meta.env.DEV) console.log('🔍 Querying price via runtime API...');
|
||||||
const poolId = assetHubApi.createType('(u32, u32)', [asset1, asset2]);
|
|
||||||
if (import.meta.env.DEV) console.log('🔍 Pool ID:', poolId.toHuman());
|
|
||||||
|
|
||||||
// Create (PalletId, PoolId) tuple: ([u8; 8], (u32, u32))
|
const quote = await (assetHubApi.call as any).assetConversionApi.quotePriceExactTokensForTokens(
|
||||||
const palletIdType = assetHubApi.createType('[u8; 8]', PALLET_ID);
|
formatAssetLocation(asset1),
|
||||||
const fullTuple = assetHubApi.createType('([u8; 8], (u32, u32))', [palletIdType, poolId]);
|
formatAssetLocation(asset2),
|
||||||
|
oneUnit.toString(),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
if (import.meta.env.DEV) console.log('🔍 Full tuple encoded length:', fullTuple.toU8a().length);
|
if (import.meta.env.DEV) console.log('🔍 Quote result:', quote?.toHuman?.() || quote);
|
||||||
if (import.meta.env.DEV) console.log('🔍 Full tuple bytes:', Array.from(fullTuple.toU8a()));
|
|
||||||
|
|
||||||
// Hash the SCALE-encoded tuple
|
if (quote && !(quote as any).isNone) {
|
||||||
const accountHash = blake2AsU8a(fullTuple.toU8a(), 256);
|
const priceRaw = (quote as any).unwrap().toString();
|
||||||
if (import.meta.env.DEV) console.log('🔍 Account hash:', Array.from(accountHash).slice(0, 8));
|
const price = Number(BigInt(priceRaw)) / Math.pow(10, decimals1);
|
||||||
|
|
||||||
const poolAccountId = assetHubApi.createType('AccountId32', accountHash);
|
if (import.meta.env.DEV) console.log('✅ Price from runtime API:', price);
|
||||||
if (import.meta.env.DEV) console.log('🔍 Pool AccountId (NEW METHOD):', poolAccountId.toString());
|
|
||||||
|
|
||||||
// Query pool account's asset balances
|
// For AMM calculation, estimate reserves from LP supply
|
||||||
if (import.meta.env.DEV) console.log('🔍 Querying reserves for asset', asset1, 'and', asset2);
|
const lpTokenId = pool.lpToken;
|
||||||
const reserve0Query = await assetHubApi.query.assets.account(asset1, poolAccountId);
|
let lpSupply = BigInt(1);
|
||||||
const reserve1Query = await assetHubApi.query.assets.account(asset2, poolAccountId);
|
|
||||||
|
|
||||||
if (import.meta.env.DEV) console.log('🔍 Reserve0 query result:', reserve0Query.toHuman());
|
if (assetHubApi.query.poolAssets?.asset) {
|
||||||
if (import.meta.env.DEV) console.log('🔍 Reserve1 query result:', reserve1Query.toHuman());
|
const lpAssetDetails = await assetHubApi.query.poolAssets.asset(lpTokenId);
|
||||||
if (import.meta.env.DEV) console.log('🔍 Reserve0 isEmpty?', reserve0Query.isEmpty);
|
if ((lpAssetDetails as any).isSome) {
|
||||||
if (import.meta.env.DEV) console.log('🔍 Reserve1 isEmpty?', reserve1Query.isEmpty);
|
lpSupply = BigInt(((lpAssetDetails as any).unwrap() as any).supply.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const reserve0Data = reserve0Query.toJSON() as Record<string, unknown>;
|
// Estimate reserves: LP = sqrt(r0 * r1), price = r1/r0
|
||||||
const reserve1Data = reserve1Query.toJSON() as Record<string, unknown>;
|
// r0 = LP / sqrt(price), r1 = LP * sqrt(price)
|
||||||
|
const sqrtPrice = Math.sqrt(price);
|
||||||
|
const reserve0 = Number(lpSupply) / sqrtPrice / Math.pow(10, 12);
|
||||||
|
const reserve1 = Number(lpSupply) * sqrtPrice / Math.pow(10, 12);
|
||||||
|
|
||||||
if (import.meta.env.DEV) console.log('🔍 Reserve0 JSON:', reserve0Data);
|
if (import.meta.env.DEV) console.log('✅ Estimated reserves:', { reserve0, reserve1, lpSupply: lpSupply.toString() });
|
||||||
if (import.meta.env.DEV) 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();
|
|
||||||
|
|
||||||
if (import.meta.env.DEV) console.log('🔍 Raw hex balances:', { balance0Hex, balance1Hex });
|
|
||||||
|
|
||||||
// Use correct decimals for each asset
|
|
||||||
// asset1=0 (wHEZ): 12 decimals
|
|
||||||
// asset1=1 (PEZ): 12 decimals
|
|
||||||
// asset2=1000 (wUSDT): 6 decimals
|
|
||||||
const decimals0 = asset1 === 1000 ? 6 : 12; // asset1 is the smaller ID
|
|
||||||
const decimals1 = asset2 === 1000 ? 6 : 12; // asset2 is the larger ID
|
|
||||||
|
|
||||||
const reserve0 = Number(BigInt(balance0Hex)) / (10 ** decimals0);
|
|
||||||
const reserve1 = Number(BigInt(balance1Hex)) / (10 ** decimals1);
|
|
||||||
|
|
||||||
if (import.meta.env.DEV) console.log('✅ Reserves found:', { reserve0, reserve1, decimals0, decimals1 });
|
|
||||||
|
|
||||||
// Store pool reserves for AMM calculation
|
// Store pool reserves for AMM calculation
|
||||||
setPoolReserves({
|
setPoolReserves({
|
||||||
reserve0,
|
reserve0,
|
||||||
reserve1,
|
reserve1,
|
||||||
asset0: asset1, // Sorted pool always has asset1 < asset2
|
asset0: asset1,
|
||||||
asset1: asset2
|
asset1: asset2
|
||||||
});
|
});
|
||||||
|
|
||||||
// Also calculate simple exchange rate for display
|
// Calculate exchange rate based on direction
|
||||||
const rate = fromAssetId === asset1
|
const rate = fromAssetId === asset1 ? price : 1 / price;
|
||||||
? reserve1 / reserve0 // from asset1 to asset2
|
|
||||||
: reserve0 / reserve1; // from asset2 to asset1
|
|
||||||
|
|
||||||
if (import.meta.env.DEV) console.log('✅ Exchange rate:', rate, 'direction:', fromAssetId === asset1 ? 'asset1→asset2' : 'asset2→asset1');
|
if (import.meta.env.DEV) console.log('✅ Exchange rate:', rate);
|
||||||
setExchangeRate(rate);
|
setExchangeRate(rate);
|
||||||
} else {
|
} else {
|
||||||
if (import.meta.env.DEV) console.warn('⚠️ Pool has no reserves - reserve0Data:', reserve0Data, 'reserve1Data:', reserve1Data);
|
if (import.meta.env.DEV) console.warn('⚠️ No price quote available');
|
||||||
setExchangeRate(0);
|
setExchangeRate(0);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (import.meta.env.DEV) console.error('❌ Error deriving pool account:', err);
|
if (import.meta.env.DEV) console.error('❌ Error fetching price:', err);
|
||||||
setExchangeRate(0);
|
setExchangeRate(0);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -550,73 +537,85 @@ const TokenSwap = () => {
|
|||||||
// Build transaction based on token types
|
// Build transaction based on token types
|
||||||
let tx;
|
let tx;
|
||||||
|
|
||||||
|
// Use XCM MultiLocation format for swap paths
|
||||||
|
// Native HEZ: { parents: 1, interior: 'Here' }
|
||||||
|
// Assets: { parents: 0, interior: { X2: [{ PalletInstance: 50 }, { GeneralIndex: id }] } }
|
||||||
|
const nativeLocation = formatAssetLocation(NATIVE_TOKEN_ID);
|
||||||
|
const pezLocation = formatAssetLocation(1);
|
||||||
|
const usdtLocation = formatAssetLocation(1000);
|
||||||
|
|
||||||
if (fromToken === 'HEZ' && toToken === 'PEZ') {
|
if (fromToken === 'HEZ' && toToken === 'PEZ') {
|
||||||
// HEZ → PEZ: wrap(HEZ→wHEZ) then swap(wHEZ→PEZ)
|
// HEZ → PEZ: Direct swap using native token pool
|
||||||
const wrapTx = assetHubApi.tx.tokenWrapper.wrap(amountIn.toString());
|
tx = assetHubApi.tx.assetConversion.swapExactTokensForTokens(
|
||||||
// AssetKind = u32, so swap path is just [0, 1]
|
[nativeLocation, pezLocation],
|
||||||
const swapPath = [0, 1]; // wHEZ → PEZ
|
|
||||||
const swapTx = assetHubApi.tx.assetConversion.swapExactTokensForTokens(
|
|
||||||
swapPath,
|
|
||||||
amountIn.toString(),
|
amountIn.toString(),
|
||||||
minAmountOut.toString(),
|
minAmountOut.toString(),
|
||||||
selectedAccount.address,
|
selectedAccount.address,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
tx = assetHubApi.tx.utility.batchAll([wrapTx, swapTx]);
|
|
||||||
|
|
||||||
} else if (fromToken === 'PEZ' && toToken === 'HEZ') {
|
} else if (fromToken === 'PEZ' && toToken === 'HEZ') {
|
||||||
// PEZ → HEZ: swap(PEZ→wHEZ) then unwrap(wHEZ→HEZ)
|
// PEZ → HEZ: Direct swap to native token
|
||||||
// AssetKind = u32, so swap path is just [1, 0]
|
tx = assetHubApi.tx.assetConversion.swapExactTokensForTokens(
|
||||||
const swapPath = [1, 0]; // PEZ → wHEZ
|
[pezLocation, nativeLocation],
|
||||||
const swapTx = assetHubApi.tx.assetConversion.swapExactTokensForTokens(
|
|
||||||
swapPath,
|
|
||||||
amountIn.toString(),
|
amountIn.toString(),
|
||||||
minAmountOut.toString(),
|
minAmountOut.toString(),
|
||||||
selectedAccount.address,
|
selectedAccount.address,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
const unwrapTx = assetHubApi.tx.tokenWrapper.unwrap(minAmountOut.toString());
|
|
||||||
tx = assetHubApi.tx.utility.batchAll([swapTx, unwrapTx]);
|
|
||||||
|
|
||||||
} else if (fromToken === 'HEZ') {
|
} else if (fromToken === 'HEZ' && toToken === 'USDT') {
|
||||||
// HEZ → Any Asset: wrap(HEZ→wHEZ) then swap(wHEZ→Asset)
|
// HEZ → USDT: Direct swap using native token pool
|
||||||
const wrapTx = assetHubApi.tx.tokenWrapper.wrap(amountIn.toString());
|
tx = assetHubApi.tx.assetConversion.swapExactTokensForTokens(
|
||||||
// Map token symbol to asset ID
|
[nativeLocation, usdtLocation],
|
||||||
const toAssetId = toToken === 'PEZ' ? 1 : toToken === 'USDT' ? 1000 : ASSET_IDS[toToken as keyof typeof ASSET_IDS];
|
|
||||||
const swapPath = [0, toAssetId]; // wHEZ → target asset
|
|
||||||
const swapTx = assetHubApi.tx.assetConversion.swapExactTokensForTokens(
|
|
||||||
swapPath,
|
|
||||||
amountIn.toString(),
|
amountIn.toString(),
|
||||||
minAmountOut.toString(),
|
minAmountOut.toString(),
|
||||||
selectedAccount.address,
|
selectedAccount.address,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
tx = assetHubApi.tx.utility.batchAll([wrapTx, swapTx]);
|
|
||||||
|
|
||||||
} else if (toToken === 'HEZ') {
|
} else if (fromToken === 'USDT' && toToken === 'HEZ') {
|
||||||
// Any Asset → HEZ: swap(Asset→wHEZ) then unwrap(wHEZ→HEZ)
|
// USDT → HEZ: Direct swap to native token
|
||||||
// Map token symbol to asset ID
|
tx = assetHubApi.tx.assetConversion.swapExactTokensForTokens(
|
||||||
const fromAssetId = fromToken === 'PEZ' ? 1 : fromToken === 'USDT' ? 1000 : ASSET_IDS[fromToken as keyof typeof ASSET_IDS];
|
[usdtLocation, nativeLocation],
|
||||||
const swapPath = [fromAssetId, 0]; // source asset → wHEZ
|
amountIn.toString(),
|
||||||
const swapTx = assetHubApi.tx.assetConversion.swapExactTokensForTokens(
|
minAmountOut.toString(),
|
||||||
swapPath,
|
selectedAccount.address,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
} else if (fromToken === 'PEZ' && toToken === 'USDT') {
|
||||||
|
// PEZ → USDT: Multi-hop through HEZ (PEZ → HEZ → USDT)
|
||||||
|
tx = assetHubApi.tx.assetConversion.swapExactTokensForTokens(
|
||||||
|
[pezLocation, nativeLocation, usdtLocation],
|
||||||
|
amountIn.toString(),
|
||||||
|
minAmountOut.toString(),
|
||||||
|
selectedAccount.address,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
} else if (fromToken === 'USDT' && toToken === 'PEZ') {
|
||||||
|
// USDT → PEZ: Multi-hop through HEZ (USDT → HEZ → PEZ)
|
||||||
|
tx = assetHubApi.tx.assetConversion.swapExactTokensForTokens(
|
||||||
|
[usdtLocation, nativeLocation, pezLocation],
|
||||||
amountIn.toString(),
|
amountIn.toString(),
|
||||||
minAmountOut.toString(),
|
minAmountOut.toString(),
|
||||||
selectedAccount.address,
|
selectedAccount.address,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
const unwrapTx = assetHubApi.tx.tokenWrapper.unwrap(minAmountOut.toString());
|
|
||||||
tx = assetHubApi.tx.utility.batchAll([swapTx, unwrapTx]);
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Direct swap between assets (PEZ ↔ USDT, etc.)
|
// Generic swap using XCM Locations
|
||||||
// Map token symbols to asset IDs
|
const getAssetLocation = (token: string) => {
|
||||||
const fromAssetId = fromToken === 'PEZ' ? 1 : fromToken === 'USDT' ? 1000 : ASSET_IDS[fromToken as keyof typeof ASSET_IDS];
|
if (token === 'HEZ') return nativeLocation;
|
||||||
const toAssetId = toToken === 'PEZ' ? 1 : toToken === 'USDT' ? 1000 : ASSET_IDS[toToken as keyof typeof ASSET_IDS];
|
if (token === 'PEZ') return pezLocation;
|
||||||
const swapPath = [fromAssetId, toAssetId];
|
if (token === 'USDT') return usdtLocation;
|
||||||
|
const assetId = ASSET_IDS[token as keyof typeof ASSET_IDS];
|
||||||
|
return formatAssetLocation(assetId);
|
||||||
|
};
|
||||||
|
|
||||||
tx = assetHubApi.tx.assetConversion.swapExactTokensForTokens(
|
tx = assetHubApi.tx.assetConversion.swapExactTokensForTokens(
|
||||||
swapPath,
|
[getAssetLocation(fromToken), getAssetLocation(toToken)],
|
||||||
amountIn.toString(),
|
amountIn.toString(),
|
||||||
minAmountOut.toString(),
|
minAmountOut.toString(),
|
||||||
selectedAccount.address,
|
selectedAccount.address,
|
||||||
|
|||||||
Reference in New Issue
Block a user