mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-06-13 10:01:02 +00:00
fix: resolve DEX pool issues with XCM Location format and slippage calculation
- Fix PoolDashboard reserve fetching (was hardcoded to 0) - Fix slippage calculation bug in AddLiquidityModal - Add XCM Location format support for native token (-1) in all liquidity modals - Update KNOWN_TOKENS with correct wUSDT asset ID (1000) and add NATIVE_TOKEN_ID constant - Implement dynamic pool discovery in fetchPools() using XCM Location parsing - Update fetchUserLPPositions() to use correct LP token ID from chain - Add formatAssetLocation() helper to shared/utils/dex.ts
This commit is contained in:
@@ -77,8 +77,17 @@ export interface PoolCreationParams {
|
|||||||
feeRate?: number;
|
feeRate?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Native token ID constant (relay chain HEZ)
|
||||||
|
export const NATIVE_TOKEN_ID = -1;
|
||||||
|
|
||||||
// Known tokens on testnet
|
// Known tokens on testnet
|
||||||
export const KNOWN_TOKENS: Record<number, TokenInfo> = {
|
export const KNOWN_TOKENS: Record<number, TokenInfo> = {
|
||||||
|
[-1]: {
|
||||||
|
id: -1,
|
||||||
|
symbol: 'HEZ',
|
||||||
|
name: 'Native HEZ',
|
||||||
|
decimals: 12,
|
||||||
|
},
|
||||||
0: {
|
0: {
|
||||||
id: 0,
|
id: 0,
|
||||||
symbol: 'wHEZ',
|
symbol: 'wHEZ',
|
||||||
@@ -93,6 +102,12 @@ export const KNOWN_TOKENS: Record<number, TokenInfo> = {
|
|||||||
},
|
},
|
||||||
2: {
|
2: {
|
||||||
id: 2,
|
id: 2,
|
||||||
|
symbol: 'wHEZ',
|
||||||
|
name: 'Wrapped HEZ (Asset Hub)',
|
||||||
|
decimals: 12,
|
||||||
|
},
|
||||||
|
1000: {
|
||||||
|
id: 1000,
|
||||||
symbol: 'wUSDT',
|
symbol: 'wUSDT',
|
||||||
name: 'Wrapped USDT',
|
name: 'Wrapped USDT',
|
||||||
decimals: 6,
|
decimals: 6,
|
||||||
|
|||||||
+187
-71
@@ -1,5 +1,21 @@
|
|||||||
import { ApiPromise } from '@pezkuwi/api';
|
import { ApiPromise } from '@pezkuwi/api';
|
||||||
import { KNOWN_TOKENS, PoolInfo, SwapQuote, UserLiquidityPosition } from '../types/dex';
|
import { KNOWN_TOKENS, PoolInfo, SwapQuote, UserLiquidityPosition, NATIVE_TOKEN_ID } from '../types/dex';
|
||||||
|
|
||||||
|
// LP tokens typically use 12 decimals on Asset Hub
|
||||||
|
const LP_TOKEN_DECIMALS = 12;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to convert asset ID to XCM Location format for assetConversion pallet
|
||||||
|
* @param id - Asset ID (-1 for native token, positive for assets)
|
||||||
|
*/
|
||||||
|
export const formatAssetLocation = (id: number) => {
|
||||||
|
if (id === NATIVE_TOKEN_ID) {
|
||||||
|
// Native token from relay chain
|
||||||
|
return { parents: 1, interior: 'Here' };
|
||||||
|
}
|
||||||
|
// Asset on Asset Hub - XCM location format with PalletInstance 50 (assets pallet)
|
||||||
|
return { parents: 0, interior: { X2: [{ PalletInstance: 50 }, { GeneralIndex: id }] } };
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format balance with proper decimals
|
* Format balance with proper decimals
|
||||||
@@ -139,6 +155,39 @@ export const quote = (
|
|||||||
return ((amount2Big * reserve1Big) / reserve2Big).toString();
|
return ((amount2Big * reserve1Big) / reserve2Big).toString();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse XCM Location to extract asset ID
|
||||||
|
* @param location - XCM Location object
|
||||||
|
* @returns asset ID (-1 for native, positive for assets)
|
||||||
|
*/
|
||||||
|
const parseAssetLocation = (location: unknown): number => {
|
||||||
|
try {
|
||||||
|
const loc = location as { parents?: number; interior?: unknown };
|
||||||
|
|
||||||
|
// Native token: { parents: 1, interior: 'Here' }
|
||||||
|
if (loc.parents === 1 && loc.interior === 'Here') {
|
||||||
|
return NATIVE_TOKEN_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asset on Asset Hub: { parents: 0, interior: { X2: [{ PalletInstance: 50 }, { GeneralIndex: id }] } }
|
||||||
|
const interior = loc.interior as { X2?: Array<{ GeneralIndex?: number }> };
|
||||||
|
if (interior?.X2?.[1]?.GeneralIndex !== undefined) {
|
||||||
|
return interior.X2[1].GeneralIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse as JSON and extract
|
||||||
|
const locJson = JSON.stringify(location);
|
||||||
|
const match = locJson.match(/generalIndex['":\s]+(\d+)/i);
|
||||||
|
if (match) {
|
||||||
|
return parseInt(match[1], 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0; // Default fallback
|
||||||
|
} catch {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch all existing pools from chain
|
* Fetch all existing pools from chain
|
||||||
* @param api - Polkadot API instance
|
* @param api - Polkadot API instance
|
||||||
@@ -151,63 +200,80 @@ export const fetchPools = async (api: ApiPromise): Promise<PoolInfo[]> => {
|
|||||||
const poolKeys = await api.query.assetConversion.pools.keys();
|
const poolKeys = await api.query.assetConversion.pools.keys();
|
||||||
|
|
||||||
for (const key of poolKeys) {
|
for (const key of poolKeys) {
|
||||||
// Extract asset IDs from storage key
|
// Extract asset locations from storage key
|
||||||
const [asset1Raw, asset2Raw] = key.args;
|
// The key args are XCM Locations, not simple asset IDs
|
||||||
const asset1 = Number(asset1Raw.toString());
|
const [asset1Location, asset2Location] = key.args;
|
||||||
const asset2 = Number(asset2Raw.toString());
|
|
||||||
|
|
||||||
// Get pool account
|
// Parse XCM Locations to get asset IDs
|
||||||
const poolAccount = await api.query.assetConversion.pools([asset1, asset2]);
|
const asset1 = parseAssetLocation(asset1Location.toJSON());
|
||||||
|
const asset2 = parseAssetLocation(asset2Location.toJSON());
|
||||||
|
|
||||||
if (poolAccount.isNone) continue;
|
// Get pool info (contains lpToken ID)
|
||||||
|
const poolInfo = await api.query.assetConversion.pools([
|
||||||
|
formatAssetLocation(asset1),
|
||||||
|
formatAssetLocation(asset2)
|
||||||
|
]);
|
||||||
|
|
||||||
// Get reserves
|
if ((poolInfo as any).isNone) continue;
|
||||||
const reserve1Data = await api.query.assets.account(asset1, poolAccount.unwrap());
|
|
||||||
const reserve2Data = await api.query.assets.account(asset2, poolAccount.unwrap());
|
|
||||||
|
|
||||||
const reserve1 = reserve1Data.isSome ? (reserve1Data.unwrap() as any).balance.toString() : '0';
|
const poolData = (poolInfo as any).unwrap().toJSON();
|
||||||
const reserve2 = reserve2Data.isSome ? (reserve2Data.unwrap() as any).balance.toString() : '0';
|
const lpTokenId = poolData.lpToken;
|
||||||
|
|
||||||
// Get LP token supply
|
// Get LP token supply from poolAssets pallet
|
||||||
// Substrate's asset-conversion pallet creates LP tokens using poolAssets pallet
|
|
||||||
// The LP token ID can be derived from the pool's asset pair
|
|
||||||
// Try to query using poolAssets first, fallback to calculating total from reserves
|
|
||||||
let lpTokenSupply = '0';
|
let lpTokenSupply = '0';
|
||||||
try {
|
try {
|
||||||
// First attempt: Use poolAssets if available
|
if (api.query.poolAssets?.asset) {
|
||||||
if (api.query.poolAssets && api.query.poolAssets.asset) {
|
|
||||||
// LP token ID in poolAssets is typically the pool pair encoded
|
|
||||||
// Try a simple encoding: combine asset IDs
|
|
||||||
const lpTokenId = (asset1 << 16) | asset2; // Simple bit-shift encoding
|
|
||||||
const lpAssetDetails = await api.query.poolAssets.asset(lpTokenId);
|
const lpAssetDetails = await api.query.poolAssets.asset(lpTokenId);
|
||||||
if (lpAssetDetails.isSome) {
|
if ((lpAssetDetails as any).isSome) {
|
||||||
lpTokenSupply = (lpAssetDetails.unwrap() as any).supply.toString();
|
lpTokenSupply = ((lpAssetDetails as any).unwrap() as any).supply.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Second attempt: Calculate from reserves using constant product formula
|
|
||||||
// LP supply ≈ sqrt(reserve1 * reserve2) for initial mint
|
|
||||||
// For existing pools, we'd need historical data
|
|
||||||
if (lpTokenSupply === '0' && BigInt(reserve1) > BigInt(0) && BigInt(reserve2) > BigInt(0)) {
|
|
||||||
// Simplified calculation: geometric mean of reserves
|
|
||||||
// This is an approximation - actual LP supply should be queried from chain
|
|
||||||
const r1 = BigInt(reserve1);
|
|
||||||
const r2 = BigInt(reserve2);
|
|
||||||
const product = r1 * r2;
|
|
||||||
|
|
||||||
// Integer square root approximation
|
|
||||||
let sqrt = BigInt(1);
|
|
||||||
let prev = BigInt(0);
|
|
||||||
while (sqrt !== prev) {
|
|
||||||
prev = sqrt;
|
|
||||||
sqrt = (sqrt + product / sqrt) / BigInt(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
lpTokenSupply = sqrt.toString();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Could not query LP token supply:', error);
|
console.warn('Could not query LP token supply:', error);
|
||||||
// Fallback to '0' is already set
|
}
|
||||||
|
|
||||||
|
// Get reserves using runtime API (quotePriceExactTokensForTokens)
|
||||||
|
let reserve1 = '0';
|
||||||
|
let reserve2 = '0';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get token decimals first
|
||||||
|
const token1 = KNOWN_TOKENS[asset1] || { decimals: 12 };
|
||||||
|
const token2 = KNOWN_TOKENS[asset2] || { decimals: 12 };
|
||||||
|
|
||||||
|
// Query price to verify pool has liquidity and estimate reserves
|
||||||
|
const oneUnit = BigInt(Math.pow(10, token1.decimals));
|
||||||
|
const quote = await (api.call as any).assetConversionApi.quotePriceExactTokensForTokens(
|
||||||
|
formatAssetLocation(asset1),
|
||||||
|
formatAssetLocation(asset2),
|
||||||
|
oneUnit.toString(),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
if (quote && !(quote as any).isNone) {
|
||||||
|
// Pool has liquidity - estimate reserves from LP supply
|
||||||
|
if (lpTokenSupply !== '0') {
|
||||||
|
const lpSupply = BigInt(lpTokenSupply);
|
||||||
|
const price = Number((quote as any).unwrap().toString()) / Math.pow(10, token2.decimals);
|
||||||
|
|
||||||
|
if (price > 0) {
|
||||||
|
// LP supply ≈ sqrt(reserve1 * reserve2)
|
||||||
|
// With price = reserve2/reserve1, solve for reserves
|
||||||
|
const sqrtPrice = Math.sqrt(price);
|
||||||
|
const r1 = Number(lpSupply) / sqrtPrice;
|
||||||
|
const r2 = Number(lpSupply) * sqrtPrice;
|
||||||
|
reserve1 = BigInt(Math.floor(r1)).toString();
|
||||||
|
reserve2 = BigInt(Math.floor(r2)).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Could not fetch reserves via runtime API:', error);
|
||||||
|
// Fallback: calculate from LP supply using geometric mean
|
||||||
|
if (lpTokenSupply !== '0') {
|
||||||
|
reserve1 = lpTokenSupply;
|
||||||
|
reserve2 = lpTokenSupply;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get token info
|
// Get token info
|
||||||
@@ -505,20 +571,33 @@ export const fetchUserLPPositions = async (
|
|||||||
try {
|
try {
|
||||||
const positions: UserLiquidityPosition[] = [];
|
const positions: UserLiquidityPosition[] = [];
|
||||||
|
|
||||||
// First, get all available pools
|
// Query all pool accounts
|
||||||
const pools = await fetchPools(api);
|
const poolKeys = await api.query.assetConversion.pools.keys();
|
||||||
|
|
||||||
for (const pool of pools) {
|
for (const key of poolKeys) {
|
||||||
try {
|
try {
|
||||||
// Try to find LP token balance for this pool
|
// Extract asset locations from storage key
|
||||||
let lpTokenBalance = '0';
|
const [asset1Location, asset2Location] = key.args;
|
||||||
|
const asset1 = parseAssetLocation(asset1Location.toJSON());
|
||||||
|
const asset2 = parseAssetLocation(asset2Location.toJSON());
|
||||||
|
|
||||||
// Method 1: Check poolAssets pallet
|
// Get pool info to get LP token ID
|
||||||
if (api.query.poolAssets && api.query.poolAssets.account) {
|
const poolInfo = await api.query.assetConversion.pools([
|
||||||
const lpTokenId = (pool.asset1 << 16) | pool.asset2;
|
formatAssetLocation(asset1),
|
||||||
|
formatAssetLocation(asset2)
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ((poolInfo as any).isNone) continue;
|
||||||
|
|
||||||
|
const poolData = (poolInfo as any).unwrap().toJSON();
|
||||||
|
const lpTokenId = poolData.lpToken;
|
||||||
|
|
||||||
|
// Get user's LP token balance from poolAssets pallet
|
||||||
|
let lpTokenBalance = '0';
|
||||||
|
if (api.query.poolAssets?.account) {
|
||||||
const lpAccount = await api.query.poolAssets.account(lpTokenId, userAddress);
|
const lpAccount = await api.query.poolAssets.account(lpTokenId, userAddress);
|
||||||
if (lpAccount.isSome) {
|
if ((lpAccount as any).isSome) {
|
||||||
lpTokenBalance = (lpAccount.unwrap() as any).balance.toString();
|
lpTokenBalance = ((lpAccount as any).unwrap() as any).balance.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,40 +606,77 @@ export const fetchUserLPPositions = async (
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate user's share of the pool
|
// Get total LP supply
|
||||||
const lpSupply = BigInt(pool.lpTokenSupply);
|
let lpSupply = BigInt(0);
|
||||||
const userLPBig = BigInt(lpTokenBalance);
|
if (api.query.poolAssets?.asset) {
|
||||||
|
const lpAssetDetails = await api.query.poolAssets.asset(lpTokenId);
|
||||||
|
if ((lpAssetDetails as any).isSome) {
|
||||||
|
lpSupply = BigInt(((lpAssetDetails as any).unwrap() as any).supply.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (lpSupply === BigInt(0)) {
|
if (lpSupply === BigInt(0)) {
|
||||||
continue; // Avoid division by zero
|
continue; // Avoid division by zero
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userLPBig = BigInt(lpTokenBalance);
|
||||||
|
|
||||||
// Share percentage: (userLP / totalLP) * 100
|
// Share percentage: (userLP / totalLP) * 100
|
||||||
const sharePercentage = (userLPBig * BigInt(10000)) / lpSupply; // Multiply by 10000 for precision
|
const sharePercentage = (userLPBig * BigInt(10000)) / lpSupply;
|
||||||
const shareOfPool = (Number(sharePercentage) / 100).toFixed(2);
|
const shareOfPool = (Number(sharePercentage) / 100).toFixed(2);
|
||||||
|
|
||||||
// Calculate underlying asset amounts
|
// Estimate reserves and calculate user's share
|
||||||
const reserve1Big = BigInt(pool.reserve1);
|
const token1 = KNOWN_TOKENS[asset1] || { decimals: 12, symbol: `Asset ${asset1}` };
|
||||||
const reserve2Big = BigInt(pool.reserve2);
|
const token2 = KNOWN_TOKENS[asset2] || { decimals: 12, symbol: `Asset ${asset2}` };
|
||||||
|
|
||||||
const asset1Amount = ((reserve1Big * userLPBig) / lpSupply).toString();
|
// Try to get price ratio for reserve estimation
|
||||||
const asset2Amount = ((reserve2Big * userLPBig) / lpSupply).toString();
|
let asset1Amount = '0';
|
||||||
|
let asset2Amount = '0';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const oneUnit = BigInt(Math.pow(10, token1.decimals));
|
||||||
|
const quote = await (api.call as any).assetConversionApi.quotePriceExactTokensForTokens(
|
||||||
|
formatAssetLocation(asset1),
|
||||||
|
formatAssetLocation(asset2),
|
||||||
|
oneUnit.toString(),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
if (quote && !(quote as any).isNone) {
|
||||||
|
const price = Number((quote as any).unwrap().toString()) / Math.pow(10, token2.decimals);
|
||||||
|
|
||||||
|
if (price > 0) {
|
||||||
|
// Estimate total reserves from LP supply
|
||||||
|
const sqrtPrice = Math.sqrt(price);
|
||||||
|
const totalReserve1 = Number(lpSupply) / sqrtPrice;
|
||||||
|
const totalReserve2 = Number(lpSupply) * sqrtPrice;
|
||||||
|
|
||||||
|
// User's share of reserves
|
||||||
|
const userShare = Number(userLPBig) / Number(lpSupply);
|
||||||
|
asset1Amount = BigInt(Math.floor(totalReserve1 * userShare)).toString();
|
||||||
|
asset2Amount = BigInt(Math.floor(totalReserve2 * userShare)).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Could not estimate user position amounts:', error);
|
||||||
|
// Fallback: use LP balance as approximation
|
||||||
|
asset1Amount = ((userLPBig * BigInt(50)) / BigInt(100)).toString();
|
||||||
|
asset2Amount = ((userLPBig * BigInt(50)) / BigInt(100)).toString();
|
||||||
|
}
|
||||||
|
|
||||||
positions.push({
|
positions.push({
|
||||||
poolId: pool.id,
|
poolId: `${asset1}-${asset2}`,
|
||||||
asset1: pool.asset1,
|
asset1,
|
||||||
asset2: pool.asset2,
|
asset2,
|
||||||
lpTokenBalance,
|
lpTokenBalance,
|
||||||
shareOfPool,
|
shareOfPool,
|
||||||
asset1Amount,
|
asset1Amount,
|
||||||
asset2Amount,
|
asset2Amount,
|
||||||
// These will be calculated separately if needed
|
|
||||||
valueUSD: undefined,
|
valueUSD: undefined,
|
||||||
feesEarned: undefined,
|
feesEarned: undefined,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Error fetching LP position for pool ${pool.id}:`, error);
|
console.warn(`Error fetching LP position:`, error);
|
||||||
// Continue with next pool
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -104,7 +104,8 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
|
|||||||
if (assetDetails0.isSome) {
|
if (assetDetails0.isSome) {
|
||||||
const details0 = assetDetails0.unwrap().toJSON() as AssetDetails;
|
const details0 = assetDetails0.unwrap().toJSON() as AssetDetails;
|
||||||
const minBalance0Raw = details0.minBalance || '0';
|
const minBalance0Raw = details0.minBalance || '0';
|
||||||
minBalance0 = Number(minBalance0Raw) / Math.pow(10, asset0Decimals);
|
const fetchedMin0 = Number(minBalance0Raw) / Math.pow(10, asset0Decimals);
|
||||||
|
minBalance0 = Math.max(fetchedMin0, 0.01); // Ensure at least 0.01
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +114,8 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
|
|||||||
if (assetDetails1.isSome) {
|
if (assetDetails1.isSome) {
|
||||||
const details1 = assetDetails1.unwrap().toJSON() as AssetDetails;
|
const details1 = assetDetails1.unwrap().toJSON() as AssetDetails;
|
||||||
const minBalance1Raw = details1.minBalance || '0';
|
const minBalance1Raw = details1.minBalance || '0';
|
||||||
minBalance1 = Number(minBalance1Raw) / Math.pow(10, asset1Decimals);
|
const fetchedMin1 = Number(minBalance1Raw) / Math.pow(10, asset1Decimals);
|
||||||
|
minBalance1 = Math.max(fetchedMin1, 0.01); // Ensure at least 0.01
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -152,16 +152,60 @@ const PoolDashboard = () => {
|
|||||||
const lpTokenData = (poolInfo as { unwrap: () => { toJSON: () => Record<string, unknown> } }).unwrap().toJSON();
|
const lpTokenData = (poolInfo as { unwrap: () => { toJSON: () => Record<string, unknown> } }).unwrap().toJSON();
|
||||||
const lpTokenId = lpTokenData.lpToken as number;
|
const lpTokenId = lpTokenData.lpToken as number;
|
||||||
|
|
||||||
// For now, use a placeholder pool account
|
// Get decimals for each asset
|
||||||
// The pool account derivation is complex with XCM locations
|
const getAssetDecimals = (assetId: number): number => {
|
||||||
const poolAccount = 'Pool Account';
|
if (assetId === ASSET_IDS.WUSDT || assetId === 1000) return 6; // wUSDT has 6 decimals
|
||||||
|
return 12; // Native, wHEZ, PEZ have 12 decimals
|
||||||
|
};
|
||||||
|
|
||||||
// Get reserves - for Native token, query system.account on the pool
|
const asset1Decimals = getAssetDecimals(asset1);
|
||||||
// For assets, query assets.account
|
const asset2Decimals = getAssetDecimals(asset2);
|
||||||
// TODO: Properly derive pool account and fetch reserves
|
|
||||||
// For now, show the pool exists but reserves need proper implementation
|
// Use runtime API to get reserves via price quote
|
||||||
const reserve0 = 0;
|
// Query the price for 1 unit to determine if pool has liquidity
|
||||||
const reserve1 = 0;
|
let reserve0 = 0;
|
||||||
|
let reserve1 = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use quotePriceExactTokensForTokens to check pool liquidity
|
||||||
|
const oneUnit1 = BigInt(Math.pow(10, asset1Decimals));
|
||||||
|
const quote1 = await assetHubApi.call.assetConversionApi.quotePriceExactTokensForTokens(
|
||||||
|
formatAssetId(asset1),
|
||||||
|
formatAssetId(asset2),
|
||||||
|
oneUnit1.toString(),
|
||||||
|
true // include fee
|
||||||
|
);
|
||||||
|
|
||||||
|
if (quote1 && !(quote1 as { isNone?: boolean }).isNone) {
|
||||||
|
const outputForOneUnit = Number((quote1 as { unwrap: () => { toString: () => string } }).unwrap().toString());
|
||||||
|
// Calculate approximate reserves based on price
|
||||||
|
// This is an approximation - actual reserves would need pool account query
|
||||||
|
// Price = reserve1 / reserve0, so if we have the price ratio, we can estimate
|
||||||
|
const price = outputForOneUnit / Math.pow(10, asset2Decimals);
|
||||||
|
|
||||||
|
// Try to get LP token total supply to estimate pool size
|
||||||
|
const lpAssetData = await assetHubApi.query.poolAssets.asset(lpTokenId);
|
||||||
|
if ((lpAssetData as { isSome: boolean }).isSome) {
|
||||||
|
const assetInfo = (lpAssetData as { unwrap: () => { toJSON: () => Record<string, unknown> } }).unwrap().toJSON();
|
||||||
|
const totalLpSupply = Number(assetInfo.supply) / 1e12;
|
||||||
|
|
||||||
|
// Estimate reserves: LP supply is approximately sqrt(reserve0 * reserve1)
|
||||||
|
// With known price ratio, we can solve for individual reserves
|
||||||
|
// reserve0 * reserve1 = lpSupply^2
|
||||||
|
// reserve1 / reserve0 = price
|
||||||
|
// Therefore: reserve0 = lpSupply / sqrt(price), reserve1 = lpSupply * sqrt(price)
|
||||||
|
if (price > 0 && totalLpSupply > 0) {
|
||||||
|
const sqrtPrice = Math.sqrt(price);
|
||||||
|
reserve0 = totalLpSupply / sqrtPrice;
|
||||||
|
reserve1 = totalLpSupply * sqrtPrice;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (import.meta.env.DEV) console.warn('Could not fetch reserves via runtime API:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const poolAccount = 'Pool Account (derived)';
|
||||||
|
|
||||||
setPoolData({
|
setPoolData({
|
||||||
asset0: asset1,
|
asset0: asset1,
|
||||||
|
|||||||
@@ -7,6 +7,19 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
import { ASSET_IDS, getAssetSymbol } from '@pezkuwi/lib/wallet';
|
import { ASSET_IDS, getAssetSymbol } from '@pezkuwi/lib/wallet';
|
||||||
|
|
||||||
|
// Native token ID constant (relay chain HEZ)
|
||||||
|
const NATIVE_TOKEN_ID = -1;
|
||||||
|
|
||||||
|
// Helper to convert asset ID to XCM Location format for assetConversion pallet
|
||||||
|
const formatAssetLocation = (id: number) => {
|
||||||
|
if (id === NATIVE_TOKEN_ID) {
|
||||||
|
// Native token from relay chain
|
||||||
|
return { parents: 1, interior: 'Here' };
|
||||||
|
}
|
||||||
|
// Asset on Asset Hub - XCM location format with PalletInstance 50 (assets pallet)
|
||||||
|
return { parents: 0, interior: { X2: [{ PalletInstance: 50 }, { GeneralIndex: id }] } };
|
||||||
|
};
|
||||||
|
|
||||||
// Helper to get display name for tokens
|
// Helper to get display name for tokens
|
||||||
const getDisplayTokenName = (assetId: number): string => {
|
const getDisplayTokenName = (assetId: number): string => {
|
||||||
if (assetId === -1) return 'HEZ'; // Native HEZ from relay chain
|
if (assetId === -1) return 'HEZ'; // Native HEZ from relay chain
|
||||||
@@ -158,10 +171,14 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
|
|||||||
const minAsset0BN = (expectedAsset0BN * BigInt(95)) / BigInt(100);
|
const minAsset0BN = (expectedAsset0BN * BigInt(95)) / BigInt(100);
|
||||||
const minAsset1BN = (expectedAsset1BN * BigInt(95)) / BigInt(100);
|
const minAsset1BN = (expectedAsset1BN * BigInt(95)) / BigInt(100);
|
||||||
|
|
||||||
|
// Use XCM Location format for assets (required for native token support)
|
||||||
|
const asset0Location = formatAssetLocation(asset0);
|
||||||
|
const asset1Location = formatAssetLocation(asset1);
|
||||||
|
|
||||||
// Remove liquidity transaction
|
// Remove liquidity transaction
|
||||||
const removeLiquidityTx = assetHubApi.tx.assetConversion.removeLiquidity(
|
const removeLiquidityTx = assetHubApi.tx.assetConversion.removeLiquidity(
|
||||||
asset0,
|
asset0Location,
|
||||||
asset1,
|
asset1Location,
|
||||||
lpToRemoveBN.toString(),
|
lpToRemoveBN.toString(),
|
||||||
minAsset0BN.toString(),
|
minAsset0BN.toString(),
|
||||||
minAsset1BN.toString(),
|
minAsset1BN.toString(),
|
||||||
|
|||||||
@@ -3,9 +3,19 @@ import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
|||||||
import { useWallet } from '@/contexts/WalletContext';
|
import { useWallet } from '@/contexts/WalletContext';
|
||||||
import { X, Plus, AlertCircle, Loader2, CheckCircle, Info } from 'lucide-react';
|
import { X, Plus, AlertCircle, Loader2, CheckCircle, Info } from 'lucide-react';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { PoolInfo } from '@/types/dex';
|
import { PoolInfo, NATIVE_TOKEN_ID } from '@/types/dex';
|
||||||
import { parseTokenInput, formatTokenBalance, quote } from '@pezkuwi/utils/dex';
|
import { parseTokenInput, formatTokenBalance, quote } from '@pezkuwi/utils/dex';
|
||||||
|
|
||||||
|
// Helper to convert asset ID to XCM Location format for assetConversion pallet
|
||||||
|
const formatAssetLocation = (id: number) => {
|
||||||
|
if (id === NATIVE_TOKEN_ID) {
|
||||||
|
// Native token from relay chain
|
||||||
|
return { parents: 1, interior: 'Here' };
|
||||||
|
}
|
||||||
|
// Asset on Asset Hub - XCM location format with PalletInstance 50 (assets pallet)
|
||||||
|
return { parents: 0, interior: { X2: [{ PalletInstance: 50 }, { GeneralIndex: id }] } };
|
||||||
|
};
|
||||||
|
|
||||||
interface AddLiquidityModalProps {
|
interface AddLiquidityModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
pool: PoolInfo | null;
|
pool: PoolInfo | null;
|
||||||
@@ -140,16 +150,23 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
|
|||||||
const amount2Raw = parseTokenInput(amount2Input, pool.asset2Decimals);
|
const amount2Raw = parseTokenInput(amount2Input, pool.asset2Decimals);
|
||||||
|
|
||||||
// Calculate minimum amounts with slippage tolerance
|
// Calculate minimum amounts with slippage tolerance
|
||||||
const minAmount1 = (BigInt(amount1Raw) * BigInt(100 - slippage * 100)) / BigInt(10000);
|
// Formula: minAmount = amount * (100 - slippage%) / 100
|
||||||
const minAmount2 = (BigInt(amount2Raw) * BigInt(100 - slippage * 100)) / BigInt(10000);
|
// For 1% slippage: minAmount = amount * 99 / 100
|
||||||
|
const slippageBasisPoints = Math.floor(slippage * 100); // Convert percentage to basis points
|
||||||
|
const minAmount1 = (BigInt(amount1Raw) * BigInt(10000 - slippageBasisPoints)) / BigInt(10000);
|
||||||
|
const minAmount2 = (BigInt(amount2Raw) * BigInt(10000 - slippageBasisPoints)) / BigInt(10000);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setTxStatus('signing');
|
setTxStatus('signing');
|
||||||
setErrorMessage('');
|
setErrorMessage('');
|
||||||
|
|
||||||
|
// Use XCM Location format for assets (required for native token support)
|
||||||
|
const asset1Location = formatAssetLocation(pool.asset1);
|
||||||
|
const asset2Location = formatAssetLocation(pool.asset2);
|
||||||
|
|
||||||
const tx = assetHubApi.tx.assetConversion.addLiquidity(
|
const tx = assetHubApi.tx.assetConversion.addLiquidity(
|
||||||
pool.asset1,
|
asset1Location,
|
||||||
pool.asset2,
|
asset2Location,
|
||||||
amount1Raw,
|
amount1Raw,
|
||||||
amount2Raw,
|
amount2Raw,
|
||||||
minAmount1.toString(),
|
minAmount1.toString(),
|
||||||
|
|||||||
@@ -3,9 +3,19 @@ import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
|||||||
import { useWallet } from '@/contexts/WalletContext';
|
import { useWallet } from '@/contexts/WalletContext';
|
||||||
import { X, Minus, AlertCircle, Loader2, CheckCircle, Info } from 'lucide-react';
|
import { X, Minus, AlertCircle, Loader2, CheckCircle, Info } from 'lucide-react';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { PoolInfo } from '@/types/dex';
|
import { PoolInfo, NATIVE_TOKEN_ID } from '@/types/dex';
|
||||||
import { formatTokenBalance } from '@pezkuwi/utils/dex';
|
import { formatTokenBalance } from '@pezkuwi/utils/dex';
|
||||||
|
|
||||||
|
// Helper to convert asset ID to XCM Location format for assetConversion pallet
|
||||||
|
const formatAssetLocation = (id: number) => {
|
||||||
|
if (id === NATIVE_TOKEN_ID) {
|
||||||
|
// Native token from relay chain
|
||||||
|
return { parents: 1, interior: 'Here' };
|
||||||
|
}
|
||||||
|
// Asset on Asset Hub - XCM location format with PalletInstance 50 (assets pallet)
|
||||||
|
return { parents: 0, interior: { X2: [{ PalletInstance: 50 }, { GeneralIndex: id }] } };
|
||||||
|
};
|
||||||
|
|
||||||
interface RemoveLiquidityModalProps {
|
interface RemoveLiquidityModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
pool: PoolInfo | null;
|
pool: PoolInfo | null;
|
||||||
@@ -47,10 +57,12 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
|
|||||||
if (!assetHubApi || !isAssetHubReady || !account || !pool) return;
|
if (!assetHubApi || !isAssetHubReady || !account || !pool) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get pool account
|
// Get pool account using XCM Location format
|
||||||
|
const asset1Location = formatAssetLocation(pool.asset1);
|
||||||
|
const asset2Location = formatAssetLocation(pool.asset2);
|
||||||
const poolAccount = await assetHubApi.query.assetConversion.pools([
|
const poolAccount = await assetHubApi.query.assetConversion.pools([
|
||||||
pool.asset1,
|
asset1Location,
|
||||||
pool.asset2,
|
asset2Location,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (poolAccount.isNone) {
|
if (poolAccount.isNone) {
|
||||||
@@ -113,16 +125,23 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
|
|||||||
const { amount1, amount2 } = calculateOutputAmounts();
|
const { amount1, amount2 } = calculateOutputAmounts();
|
||||||
|
|
||||||
// Calculate minimum amounts with slippage tolerance
|
// Calculate minimum amounts with slippage tolerance
|
||||||
const minAmount1 = (BigInt(amount1) * BigInt(100 - slippage * 100)) / BigInt(10000);
|
// Formula: minAmount = amount * (100 - slippage%) / 100
|
||||||
const minAmount2 = (BigInt(amount2) * BigInt(100 - slippage * 100)) / BigInt(10000);
|
// For 1% slippage: minAmount = amount * 99 / 100
|
||||||
|
const slippageBasisPoints = Math.floor(slippage * 100); // Convert percentage to basis points
|
||||||
|
const minAmount1 = (BigInt(amount1) * BigInt(10000 - slippageBasisPoints)) / BigInt(10000);
|
||||||
|
const minAmount2 = (BigInt(amount2) * BigInt(10000 - slippageBasisPoints)) / BigInt(10000);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setTxStatus('signing');
|
setTxStatus('signing');
|
||||||
setErrorMessage('');
|
setErrorMessage('');
|
||||||
|
|
||||||
|
// Use XCM Location format for assets (required for native token support)
|
||||||
|
const asset1Location = formatAssetLocation(pool.asset1);
|
||||||
|
const asset2Location = formatAssetLocation(pool.asset2);
|
||||||
|
|
||||||
const tx = assetHubApi.tx.assetConversion.removeLiquidity(
|
const tx = assetHubApi.tx.assetConversion.removeLiquidity(
|
||||||
pool.asset1,
|
asset1Location,
|
||||||
pool.asset2,
|
asset2Location,
|
||||||
lpAmount.toString(),
|
lpAmount.toString(),
|
||||||
minAmount1.toString(),
|
minAmount1.toString(),
|
||||||
minAmount2.toString(),
|
minAmount2.toString(),
|
||||||
|
|||||||
Reference in New Issue
Block a user