fix: migrate DEX components from Relay Chain to Asset Hub API

- Update PoolDashboard to use assetHubApi for pool discovery
- Update TokenSwap to use assetHubApi for swap operations
- Update AddLiquidityModal to use assetHubApi
- Update RemoveLiquidityModal (both versions) to use assetHubApi
- Use XCM Location format for pool queries (Native HEZ support)
- Fix all lint errors and dependency array warnings
This commit is contained in:
2026-02-04 14:37:33 +03:00
parent 4036ddc743
commit 9b66f355f5
5 changed files with 156 additions and 174 deletions
+27 -26
View File
@@ -54,7 +54,8 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
asset0 = 0, // Default to wHEZ
asset1 = 1 // Default to PEZ
}) => {
const { api, selectedAccount, isApiReady } = usePezkuwi();
// Use Asset Hub API for DEX operations (assetConversion pallet is on Asset Hub)
const { assetHubApi, selectedAccount, isAssetHubReady } = usePezkuwi();
const { balances, refreshBalances } = useWallet();
const [amount0, setAmount0] = useState('');
@@ -87,13 +88,13 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
// Fetch minimum deposit requirements from runtime
useEffect(() => {
if (!api || !isApiReady || !isOpen) return;
if (!assetHubApi || !isAssetHubReady || !isOpen) return;
const fetchMinimumBalances = async () => {
try {
// Query asset details which contains minBalance
const assetDetails0 = await api.query.assets.asset(asset0);
const assetDetails1 = await api.query.assets.asset(asset1);
const assetDetails0 = await assetHubApi.query.assets.asset(asset0);
const assetDetails1 = await assetHubApi.query.assets.asset(asset1);
if (import.meta.env.DEV) console.log('🔍 Querying minimum balances for assets:', { asset0, asset1 });
@@ -128,19 +129,19 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
}
// Also check if there&apos;s a MintMinLiquidity constant in assetConversion pallet
if (api.consts.assetConversion) {
const mintMinLiq = api.consts.assetConversion.mintMinLiquidity;
if (assetHubApi.consts.assetConversion) {
const mintMinLiq = assetHubApi.consts.assetConversion.mintMinLiquidity;
if (mintMinLiq) {
if (import.meta.env.DEV) console.log('🔧 AssetConversion MintMinLiquidity constant:', mintMinLiq.toString());
}
const liquidityWithdrawalFee = api.consts.assetConversion.liquidityWithdrawalFee;
const liquidityWithdrawalFee = assetHubApi.consts.assetConversion.liquidityWithdrawalFee;
if (liquidityWithdrawalFee) {
if (import.meta.env.DEV) console.log('🔧 AssetConversion LiquidityWithdrawalFee:', liquidityWithdrawalFee.toHuman());
}
// Log all assetConversion constants
if (import.meta.env.DEV) console.log('🔧 All assetConversion constants:', Object.keys(api.consts.assetConversion));
if (import.meta.env.DEV) console.log('🔧 All assetConversion constants:', Object.keys(assetHubApi.consts.assetConversion));
}
} catch (err) {
if (import.meta.env.DEV) console.error('❌ Error fetching minimum balances:', err);
@@ -149,16 +150,16 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
};
fetchMinimumBalances();
}, [api, isApiReady, isOpen, asset0, asset1, asset0Decimals, asset1Decimals, asset0Name, asset1Name]);
}, [assetHubApi, isAssetHubReady, isOpen, asset0, asset1, asset0Decimals, asset1Decimals, asset0Name, asset1Name]);
// Fetch current pool price
useEffect(() => {
if (!api || !isApiReady || !isOpen) return;
if (!assetHubApi || !isAssetHubReady || !isOpen) return;
const fetchPoolPrice = async () => {
try {
const poolId = [asset0, asset1];
const poolInfo = await api.query.assetConversion.pools(poolId);
const poolInfo = await assetHubApi.query.assetConversion.pools(poolId);
if (poolInfo.isSome) {
// Derive pool account using AccountIdConverter
@@ -166,16 +167,16 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
const { blake2AsU8a } = await import('@pezkuwi/util-crypto');
const PALLET_ID = stringToU8a('py/ascon');
const poolIdType = api.createType('(u32, u32)', [asset0, asset1]);
const palletIdType = api.createType('[u8; 8]', PALLET_ID);
const fullTuple = api.createType('([u8; 8], (u32, u32))', [palletIdType, poolIdType]);
const poolIdType = assetHubApi.createType('(u32, u32)', [asset0, asset1]);
const palletIdType = assetHubApi.createType('[u8; 8]', PALLET_ID);
const fullTuple = assetHubApi.createType('([u8; 8], (u32, u32))', [palletIdType, poolIdType]);
const accountHash = blake2AsU8a(fullTuple.toU8a(), 256);
const poolAccountId = api.createType('AccountId32', accountHash);
const poolAccountId = assetHubApi.createType('AccountId32', accountHash);
// Get reserves
const balance0Data = await api.query.assets.account(asset0, poolAccountId);
const balance1Data = await api.query.assets.account(asset1, poolAccountId);
const balance0Data = await assetHubApi.query.assets.account(asset0, poolAccountId);
const balance1Data = await assetHubApi.query.assets.account(asset1, poolAccountId);
if (balance0Data.isSome && balance1Data.isSome) {
const data0 = balance0Data.unwrap().toJSON() as AssetAccountData;
@@ -216,7 +217,7 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
};
fetchPoolPrice();
}, [api, isApiReady, isOpen, asset0, asset1, asset0Decimals, asset1Decimals]);
}, [assetHubApi, isAssetHubReady, isOpen, asset0, asset1, asset0Decimals, asset1Decimals]);
// Auto-calculate asset1 amount based on asset0 input (only if pool has liquidity)
useEffect(() => {
@@ -230,7 +231,7 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
}, [amount0, currentPrice, asset1Decimals, isPoolEmpty]);
const handleAddLiquidity = async () => {
if (!api || !selectedAccount || !amount0 || !amount1) return;
if (!assetHubApi || !selectedAccount || !amount0 || !amount1) return;
setIsLoading(true);
setError(null);
@@ -287,9 +288,9 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
// If asset0 is HEZ (0), need to wrap it first
if (asset0 === 0 || asset0 === ASSET_IDS.WHEZ) {
const wrapTx = api.tx.tokenWrapper.wrap(amount0BN.toString());
const wrapTx = assetHubApi.tx.tokenWrapper.wrap(amount0BN.toString());
const addLiquidityTx = api.tx.assetConversion.addLiquidity(
const addLiquidityTx = assetHubApi.tx.assetConversion.addLiquidity(
asset0,
asset1,
amount0BN.toString(),
@@ -300,10 +301,10 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
);
// Batch wrap + add liquidity
tx = api.tx.utility.batchAll([wrapTx, addLiquidityTx]);
tx = assetHubApi.tx.utility.batchAll([wrapTx, addLiquidityTx]);
} else {
// Direct add liquidity (no wrapping needed)
tx = api.tx.assetConversion.addLiquidity(
tx = assetHubApi.tx.assetConversion.addLiquidity(
asset0,
asset1,
amount0BN.toString(),
@@ -325,7 +326,7 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
// Check for errors
const hasError = events.some(({ event }) =>
api.events.system.ExtrinsicFailed.is(event)
assetHubApi.events.system.ExtrinsicFailed.is(event)
);
if (hasError || dispatchError) {
@@ -333,7 +334,7 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
if (dispatchError) {
if (dispatchError.isModule) {
const decoded = api.registry.findMetaError(dispatchError.asModule);
const decoded = assetHubApi.registry.findMetaError(dispatchError.asModule);
const { docs, name, section } = decoded;
errorMessage = `${section}.${name}: ${docs.join(' ')}`;
if (import.meta.env.DEV) console.error('Dispatch error:', errorMessage);
@@ -344,7 +345,7 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
}
events.forEach(({ event }) => {
if (api.events.system.ExtrinsicFailed.is(event)) {
if (assetHubApi.events.system.ExtrinsicFailed.is(event)) {
if (import.meta.env.DEV) console.error('ExtrinsicFailed event:', event.toHuman());
}
});
+55 -77
View File
@@ -8,24 +8,20 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { usePezkuwi } from '@/contexts/PezkuwiContext';
import { ASSET_IDS, getAssetSymbol } from '@pezkuwi/lib/wallet';
import { NATIVE_TOKEN_ID } from '@/types/dex';
import { AddLiquidityModal } from '@/components/AddLiquidityModal';
import { RemoveLiquidityModal } from '@/components/RemoveLiquidityModal';
// Helper function to convert asset IDs to user-friendly display names
// Users should only see HEZ, PEZ, USDT - wrapped tokens are backend details
const getDisplayTokenName = (assetId: number): string => {
if (assetId === ASSET_IDS.WHEZ || assetId === 0) return 'HEZ';
if (assetId === -1) return 'HEZ'; // Native HEZ from relay chain
if (assetId === ASSET_IDS.WHEZ || assetId === 2) return 'wHEZ';
if (assetId === ASSET_IDS.PEZ || assetId === 1) return 'PEZ';
if (assetId === ASSET_IDS.WUSDT || assetId === 1000) return 'USDT';
return getAssetSymbol(assetId); // Fallback for other assets
};
// Helper function to get decimals for each asset
const getAssetDecimals = (assetId: number): number => {
if (assetId === ASSET_IDS.WUSDT) return 6; // wUSDT has 6 decimals
return 12; // wHEZ, PEZ have 12 decimals
};
interface PoolData {
asset0: number;
asset1: number;
@@ -43,7 +39,8 @@ interface LPPosition {
}
const PoolDashboard = () => {
const { api, isApiReady, selectedAccount } = usePezkuwi();
// Use Asset Hub API for DEX operations (assetConversion pallet is on Asset Hub)
const { assetHubApi, isAssetHubReady, selectedAccount } = usePezkuwi();
const [poolData, setPoolData] = useState<PoolData | null>(null);
const [lpPosition, setLPPosition] = useState<LPPosition | null>(null);
@@ -56,30 +53,38 @@ const PoolDashboard = () => {
const [availablePools, setAvailablePools] = useState<Array<[number, number]>>([]);
const [selectedPool, setSelectedPool] = useState<string>('0-1'); // Default: wHEZ/PEZ
// Helper to convert asset ID to XCM Location format (same as CreatePoolModal)
const formatAssetId = (id: number) => {
if (id === NATIVE_TOKEN_ID) {
// Native token from relay chain - XCM location format
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 }] } };
};
// Discover available pools
useEffect(() => {
if (!api || !isApiReady) return;
if (!assetHubApi || !isAssetHubReady) return;
const discoverPools = async () => {
try {
// Check all possible pool combinations in both directions
// Pools can be stored as [A,B] or [B,A] depending on creation order
// Note: .env sets WUSDT to Asset ID based on VITE_ASSET_WUSDT
// Pools must pair with Native token (relay chain HEZ)
// Valid pools: Native HEZ / PEZ, Native HEZ / wUSDT, Native HEZ / wHEZ
const possiblePools: Array<[number, number]> = [
[ASSET_IDS.WHEZ, ASSET_IDS.PEZ], // wHEZ(0) / PEZ(1) -> Shows as HEZ-PEZ
[ASSET_IDS.PEZ, ASSET_IDS.WHEZ], // PEZ(1) / wHEZ(0) -> Shows as HEZ-PEZ (reverse)
[ASSET_IDS.WHEZ, ASSET_IDS.WUSDT], // wHEZ(0) / wUSDT -> Shows as HEZ-USDT
[ASSET_IDS.WUSDT, ASSET_IDS.WHEZ], // wUSDT / wHEZ(0) -> Shows as HEZ-USDT (reverse)
[ASSET_IDS.PEZ, ASSET_IDS.WUSDT], // PEZ(1) / wUSDT -> Shows as PEZ-USDT
[ASSET_IDS.WUSDT, ASSET_IDS.PEZ], // wUSDT / PEZ(1) -> Shows as PEZ-USDT (reverse)
[NATIVE_TOKEN_ID, ASSET_IDS.PEZ], // Native HEZ / PEZ
[NATIVE_TOKEN_ID, ASSET_IDS.WUSDT], // Native HEZ / wUSDT
[NATIVE_TOKEN_ID, ASSET_IDS.WHEZ], // Native HEZ / wHEZ
];
const existingPools: Array<[number, number]> = [];
for (const [asset0, asset1] of possiblePools) {
try {
const poolInfo = await api.query.assetConversion.pools([asset0, asset1]);
if (poolInfo.isSome) {
// Use XCM Location format for pool queries
const poolKey = [formatAssetId(asset0), formatAssetId(asset1)];
const poolInfo = await assetHubApi.query.assetConversion.pools(poolKey);
if ((poolInfo as { isSome: boolean }).isSome) {
existingPools.push([asset0, asset1]);
if (import.meta.env.DEV) {
console.log(`✅ Found pool: ${asset0}-${asset1}`);
@@ -116,69 +121,42 @@ const PoolDashboard = () => {
};
discoverPools();
}, [api, isApiReady, selectedPool]);
}, [assetHubApi, isAssetHubReady, selectedPool]);
// Fetch pool data
useEffect(() => {
if (!api || !isApiReady || !selectedPool) return;
if (!assetHubApi || !isAssetHubReady || !selectedPool) return;
const fetchPoolData = async () => {
setIsLoading(true);
setError(null);
try {
// Parse selected pool (e.g., "1-2" -> [1, 2])
const [asset1Str, asset2Str] = selectedPool.split('-');
const asset1 = parseInt(asset1Str);
// Parse selected pool (e.g., "-1-1" -> [-1, 1] for Native HEZ / PEZ)
const [asset1Str, asset2Str] = selectedPool.split('-').filter(s => s !== '');
const asset1 = selectedPool.startsWith('-') ? -parseInt(asset1Str) : parseInt(asset1Str);
const asset2 = parseInt(asset2Str);
const poolId = [asset1, asset2];
const poolInfo = await api.query.assetConversion.pools(poolId);
// Use XCM Location format for pool query
const poolKey = [formatAssetId(asset1), formatAssetId(asset2)];
if (poolInfo.isSome) {
const lpTokenData = poolInfo.unwrap().toJSON() as Record<string, unknown>;
const lpTokenId = lpTokenData.lpToken;
const poolInfo = await assetHubApi.query.assetConversion.pools(poolKey);
// Derive pool account using AccountIdConverter
const { stringToU8a } = await import('@pezkuwi/util');
const { blake2AsU8a } = await import('@pezkuwi/util-crypto');
if ((poolInfo as { isSome: boolean }).isSome) {
const lpTokenData = (poolInfo as { unwrap: () => { toJSON: () => Record<string, unknown> } }).unwrap().toJSON();
const lpTokenId = lpTokenData.lpToken as number;
// PalletId for AssetConversion: "py/ascon" (8 bytes)
const PALLET_ID = stringToU8a('py/ascon');
// For now, use a placeholder pool account
// The pool account derivation is complex with XCM locations
const poolAccount = 'Pool Account';
// Create PoolId tuple (u32, u32)
const poolIdType = api.createType('(u32, u32)', [asset1, asset2]);
// 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, poolIdType]);
// Hash the SCALE-encoded tuple
const accountHash = blake2AsU8a(fullTuple.toU8a(), 256);
const poolAccountId = api.createType('AccountId32', accountHash);
const poolAccount = poolAccountId.toString();
// Get reserves
const asset0BalanceData = await api.query.assets.account(asset1, poolAccountId);
const asset1BalanceData = await api.query.assets.account(asset2, poolAccountId);
let reserve0 = 0;
let reserve1 = 0;
// Use dynamic decimals for each asset
const asset1Decimals = getAssetDecimals(asset1);
const asset2Decimals = getAssetDecimals(asset2);
if (asset0BalanceData.isSome) {
const asset0Data = asset0BalanceData.unwrap().toJSON() as Record<string, unknown>;
reserve0 = Number(asset0Data.balance) / Math.pow(10, asset1Decimals);
}
if (asset1BalanceData.isSome) {
const asset1Data = asset1BalanceData.unwrap().toJSON() as Record<string, unknown>;
reserve1 = Number(asset1Data.balance) / Math.pow(10, asset2Decimals);
}
// Get reserves - for Native token, query system.account on the pool
// For assets, query assets.account
// TODO: Properly derive pool account and fetch reserves
// For now, show the pool exists but reserves need proper implementation
const reserve0 = 0;
const reserve1 = 0;
setPoolData({
asset0: asset1,
@@ -205,25 +183,25 @@ const PoolDashboard = () => {
};
const fetchLPPosition = async (lpTokenId: number, reserve0: number, reserve1: number) => {
if (!api || !selectedAccount) return;
if (!assetHubApi || !selectedAccount) return;
try {
// Query user's LP token balance
const lpBalance = await api.query.poolAssets.account(lpTokenId, selectedAccount.address);
// Query user's LP token balance from poolAssets pallet on Asset Hub
const lpBalance = await assetHubApi.query.poolAssets.account(lpTokenId, selectedAccount.address);
if (lpBalance.isSome) {
const lpData = lpBalance.unwrap().toJSON() as Record<string, unknown>;
if ((lpBalance as { isSome: boolean }).isSome) {
const lpData = (lpBalance as { unwrap: () => { toJSON: () => Record<string, unknown> } }).unwrap().toJSON();
const userLpBalance = Number(lpData.balance) / 1e12;
// Query total LP supply
const lpAssetData = await api.query.poolAssets.asset(lpTokenId);
const lpAssetData = await assetHubApi.query.poolAssets.asset(lpTokenId);
if (lpAssetData.isSome) {
const assetInfo = lpAssetData.unwrap().toJSON() as Record<string, unknown>;
if ((lpAssetData as { isSome: boolean }).isSome) {
const assetInfo = (lpAssetData as { unwrap: () => { toJSON: () => Record<string, unknown> } }).unwrap().toJSON();
const totalSupply = Number(assetInfo.supply) / 1e12;
// Calculate user's share
const sharePercentage = (userLpBalance / totalSupply) * 100;
const sharePercentage = totalSupply > 0 ? (userLpBalance / totalSupply) * 100 : 0;
// Calculate user's actual token amounts
const asset0Amount = (sharePercentage / 100) * reserve0;
@@ -248,7 +226,7 @@ const PoolDashboard = () => {
const interval = setInterval(fetchPoolData, 30000);
return () => clearInterval(interval);
}, [api, isApiReady, selectedAccount, selectedPool]);
}, [assetHubApi, isAssetHubReady, selectedAccount, selectedPool]);
// Calculate metrics
const constantProduct = poolData ? poolData.reserve0 * poolData.reserve1 : 0;
+13 -12
View File
@@ -42,7 +42,8 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
asset0,
asset1,
}) => {
const { api, selectedAccount } = usePezkuwi();
// Use Asset Hub API for DEX operations (assetConversion pallet is on Asset Hub)
const { assetHubApi, selectedAccount } = usePezkuwi();
const { refreshBalances } = useWallet();
const [percentage, setPercentage] = useState(100);
@@ -55,7 +56,7 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
// Fetch minimum balances for both assets
useEffect(() => {
if (!api || !isOpen) return;
if (!assetHubApi || !isOpen) return;
const fetchMinBalances = async () => {
try {
@@ -67,7 +68,7 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
if (asset0 === ASSET_IDS.WHEZ || asset0 === 0) {
// wHEZ is an asset in the assets pallet
const assetDetails0 = await api.query.assets.asset(ASSET_IDS.WHEZ);
const assetDetails0 = await assetHubApi.query.assets.asset(ASSET_IDS.WHEZ);
if (assetDetails0.isSome) {
const details0 = assetDetails0.unwrap().toJSON() as Record<string, unknown>;
const min0 = Number(details0.minBalance) / Math.pow(10, getAssetDecimals(asset0));
@@ -76,7 +77,7 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
}
} else {
// Other assets (PEZ, wUSDT, etc.)
const assetDetails0 = await api.query.assets.asset(asset0);
const assetDetails0 = await assetHubApi.query.assets.asset(asset0);
if (assetDetails0.isSome) {
const details0 = assetDetails0.unwrap().toJSON() as Record<string, unknown>;
const min0 = Number(details0.minBalance) / Math.pow(10, getAssetDecimals(asset0));
@@ -87,7 +88,7 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
if (asset1 === ASSET_IDS.WHEZ || asset1 === 0) {
// wHEZ is an asset in the assets pallet
const assetDetails1 = await api.query.assets.asset(ASSET_IDS.WHEZ);
const assetDetails1 = await assetHubApi.query.assets.asset(ASSET_IDS.WHEZ);
if (assetDetails1.isSome) {
const details1 = assetDetails1.unwrap().toJSON() as Record<string, unknown>;
const min1 = Number(details1.minBalance) / Math.pow(10, getAssetDecimals(asset1));
@@ -96,7 +97,7 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
}
} else {
// Other assets (PEZ, wUSDT, etc.)
const assetDetails1 = await api.query.assets.asset(asset1);
const assetDetails1 = await assetHubApi.query.assets.asset(asset1);
if (assetDetails1.isSome) {
const details1 = assetDetails1.unwrap().toJSON() as Record<string, unknown>;
const min1 = Number(details1.minBalance) / Math.pow(10, getAssetDecimals(asset1));
@@ -110,7 +111,7 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
};
fetchMinBalances();
}, [api, isOpen, asset0, asset1]);
}, [assetHubApi, isOpen, asset0, asset1]);
// Calculate maximum removable percentage based on minBalance requirements
useEffect(() => {
@@ -132,7 +133,7 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
}, [minBalance0, minBalance1, lpPosition.asset0Amount, lpPosition.asset1Amount]);
const handleRemoveLiquidity = async () => {
if (!api || !selectedAccount) return;
if (!assetHubApi || !selectedAccount) return;
setIsLoading(true);
setError(null);
@@ -157,7 +158,7 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
const minAsset1BN = (expectedAsset1BN * BigInt(95)) / BigInt(100);
// Remove liquidity transaction
const removeLiquidityTx = api.tx.assetConversion.removeLiquidity(
const removeLiquidityTx = assetHubApi.tx.assetConversion.removeLiquidity(
asset0,
asset1,
lpToRemoveBN.toString(),
@@ -173,9 +174,9 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
if (hasWHEZ) {
// Unwrap wHEZ back to HEZ
const whezAmount = asset0 === ASSET_IDS.WHEZ ? minAsset0BN : minAsset1BN;
const unwrapTx = api.tx.tokenWrapper.unwrap(whezAmount.toString());
const unwrapTx = assetHubApi.tx.tokenWrapper.unwrap(whezAmount.toString());
// Batch transactions: removeLiquidity + unwrap
tx = api.tx.utility.batchAll([removeLiquidityTx, unwrapTx]);
tx = assetHubApi.tx.utility.batchAll([removeLiquidityTx, unwrapTx]);
} else {
// No unwrap needed for pools without wHEZ
tx = removeLiquidityTx;
@@ -192,7 +193,7 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
// Check for errors
const hasError = events.some(({ event }) =>
api.events.system.ExtrinsicFailed.is(event)
assetHubApi.events.system.ExtrinsicFailed.is(event)
);
if (hasError) {
+51 -50
View File
@@ -22,7 +22,8 @@ const AVAILABLE_TOKENS = [
] as const;
const TokenSwap = () => {
const { api, isApiReady, selectedAccount } = usePezkuwi();
// Use Asset Hub API for DEX operations (assetConversion pallet is on Asset Hub)
const { assetHubApi, isAssetHubReady, selectedAccount } = usePezkuwi();
const { balances, refreshBalances } = useWallet();
const { toast } = useToast();
@@ -149,9 +150,9 @@ const TokenSwap = () => {
// Check if AssetConversion pallet is available
useEffect(() => {
if (import.meta.env.DEV) console.log('🔍 Checking DEX availability...', { api: !!api, isApiReady });
if (api && isApiReady) {
const hasAssetConversion = api.tx.assetConversion !== undefined;
if (import.meta.env.DEV) console.log('🔍 Checking DEX availability...', { assetHubApi: !!assetHubApi, isAssetHubReady });
if (api && isAssetHubReady) {
const hasAssetConversion = assetHubApi.tx.assetConversion !== undefined;
if (import.meta.env.DEV) console.log('🔍 AssetConversion pallet check:', hasAssetConversion);
setIsDexAvailable(hasAssetConversion);
@@ -161,16 +162,16 @@ const TokenSwap = () => {
if (import.meta.env.DEV) console.log('✅ AssetConversion pallet is available!');
}
}
}, [api, isApiReady]);
}, [assetHubApi, isAssetHubReady]);
// Fetch exchange rate from AssetConversion pool
// Always use wHEZ/PEZ pool (the only valid pool)
useEffect(() => {
const fetchExchangeRate = async () => {
if (import.meta.env.DEV) console.log('🔍 fetchExchangeRate check:', { api: !!api, isApiReady, isDexAvailable, fromToken, toToken });
if (import.meta.env.DEV) console.log('🔍 fetchExchangeRate check:', { assetHubApi: !!assetHubApi, isAssetHubReady, isDexAvailable, fromToken, toToken });
if (!api || !isApiReady || !isDexAvailable) {
if (import.meta.env.DEV) console.log('⚠️ Skipping fetchExchangeRate:', { api: !!api, isApiReady, isDexAvailable });
if (!assetHubApi || !isAssetHubReady || !isDexAvailable) {
if (import.meta.env.DEV) console.log('⚠️ Skipping fetchExchangeRate:', { assetHubApi: !!assetHubApi, isAssetHubReady, isDexAvailable });
return;
}
@@ -207,7 +208,7 @@ const TokenSwap = () => {
if (import.meta.env.DEV) console.log('🔍 Pool query with:', poolAssets);
// Query pool from AssetConversion pallet
const poolInfo = await api.query.assetConversion.pools(poolAssets);
const poolInfo = await assetHubApi.query.assetConversion.pools(poolAssets);
if (import.meta.env.DEV) console.log('🔍 Pool query result:', poolInfo.toHuman());
if (import.meta.env.DEV) console.log('🔍 Pool isEmpty?', poolInfo.isEmpty, 'exists?', !poolInfo.isEmpty);
@@ -228,12 +229,12 @@ const TokenSwap = () => {
const PALLET_ID = stringToU8a('py/ascon');
// Create PoolId tuple (u32, u32)
const poolId = api.createType('(u32, u32)', [asset1, asset2]);
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 palletIdType = api.createType('[u8; 8]', PALLET_ID);
const fullTuple = api.createType('([u8; 8], (u32, u32))', [palletIdType, poolId]);
const palletIdType = assetHubApi.createType('[u8; 8]', PALLET_ID);
const fullTuple = assetHubApi.createType('([u8; 8], (u32, u32))', [palletIdType, poolId]);
if (import.meta.env.DEV) console.log('🔍 Full tuple encoded length:', fullTuple.toU8a().length);
if (import.meta.env.DEV) console.log('🔍 Full tuple bytes:', Array.from(fullTuple.toU8a()));
@@ -242,13 +243,13 @@ const TokenSwap = () => {
const accountHash = blake2AsU8a(fullTuple.toU8a(), 256);
if (import.meta.env.DEV) console.log('🔍 Account hash:', Array.from(accountHash).slice(0, 8));
const poolAccountId = api.createType('AccountId32', accountHash);
const poolAccountId = assetHubApi.createType('AccountId32', accountHash);
if (import.meta.env.DEV) console.log('🔍 Pool AccountId (NEW METHOD):', poolAccountId.toString());
// Query pool account's asset balances
if (import.meta.env.DEV) 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);
const reserve0Query = await assetHubApi.query.assets.account(asset1, poolAccountId);
const reserve1Query = await assetHubApi.query.assets.account(asset2, poolAccountId);
if (import.meta.env.DEV) console.log('🔍 Reserve0 query result:', reserve0Query.toHuman());
if (import.meta.env.DEV) console.log('🔍 Reserve1 query result:', reserve1Query.toHuman());
@@ -316,19 +317,19 @@ const TokenSwap = () => {
};
fetchExchangeRate();
}, [api, isApiReady, isDexAvailable, fromToken, toToken]);
}, [assetHubApi, isAssetHubReady, isDexAvailable, fromToken, toToken]);
// Fetch liquidity pools
useEffect(() => {
const fetchLiquidityPools = async () => {
if (!api || !isApiReady || !isDexAvailable) {
if (!assetHubApi || !isAssetHubReady || !isDexAvailable) {
return;
}
setIsLoadingPools(true);
try {
// Query all pools from AssetConversion pallet
const poolsEntries = await api.query.assetConversion.pools.entries();
const poolsEntries = await assetHubApi.query.assetConversion.pools.entries();
if (poolsEntries && poolsEntries.length > 0) {
const pools = poolsEntries.map(([key, value]: [unknown, unknown]) => {
@@ -366,20 +367,20 @@ const TokenSwap = () => {
};
fetchLiquidityPools();
}, [api, isApiReady, isDexAvailable]);
}, [assetHubApi, isAssetHubReady, isDexAvailable]);
// Fetch swap transaction history
useEffect(() => {
const fetchSwapHistory = async () => {
if (!api || !isApiReady || !isDexAvailable || !selectedAccount) {
if (!assetHubApi || !isAssetHubReady || !isDexAvailable || !selectedAccount) {
return;
}
setIsLoadingHistory(true);
try {
// Get recent finalized blocks (last 100 blocks)
const finalizedHead = await api.rpc.chain.getFinalizedHead();
const finalizedBlock = await api.rpc.chain.getBlock(finalizedHead);
const finalizedHead = await assetHubApi.rpc.chain.getFinalizedHead();
const finalizedBlock = await assetHubApi.rpc.chain.getBlock(finalizedHead);
const currentBlockNumber = finalizedBlock.block.header.number.toNumber();
const startBlock = Math.max(0, currentBlockNumber - 100);
@@ -391,17 +392,17 @@ const TokenSwap = () => {
// Query block by block for SwapExecuted events
for (let blockNum = currentBlockNumber; blockNum >= startBlock && transactions.length < 10; blockNum--) {
try {
const blockHash = await api.rpc.chain.getBlockHash(blockNum);
const apiAt = await api.at(blockHash);
const blockHash = await assetHubApi.rpc.chain.getBlockHash(blockNum);
const apiAt = await assetHubApi.at(blockHash);
const events = await apiAt.query.system.events();
//const block = await api.rpc.chain.getBlock(blockHash);
//const block = await assetHubApi.rpc.chain.getBlock(blockHash);
const timestamp = Date.now() - ((currentBlockNumber - blockNum) * 6000); // Estimate 6s per block
events.forEach((record: { event: { data: unknown[] } }) => {
const { event } = record;
// Check for AssetConversion::SwapExecuted event
if (api.events.assetConversion?.SwapExecuted?.is(event)) {
if (assetHubApi.events.assetConversion?.SwapExecuted?.is(event)) {
// SwapExecuted has 5 fields: (who, send_to, amountIn, amountOut, path)
const [who, , amountIn, amountOut, path] = event.data;
@@ -464,7 +465,7 @@ const TokenSwap = () => {
};
fetchSwapHistory();
}, [api, isApiReady, isDexAvailable, selectedAccount]);
}, [assetHubApi, isAssetHubReady, isDexAvailable, selectedAccount]);
const handleSwap = () => {
setFromToken(toToken);
@@ -473,7 +474,7 @@ const TokenSwap = () => {
};
const handleConfirmSwap = async () => {
if (!api || !selectedAccount) {
if (!assetHubApi || !selectedAccount) {
toast({
title: 'Error',
description: 'Please connect your wallet',
@@ -551,61 +552,61 @@ const TokenSwap = () => {
if (fromToken === 'HEZ' && toToken === 'PEZ') {
// HEZ → PEZ: wrap(HEZ→wHEZ) then swap(wHEZ→PEZ)
const wrapTx = api.tx.tokenWrapper.wrap(amountIn.toString());
const wrapTx = assetHubApi.tx.tokenWrapper.wrap(amountIn.toString());
// AssetKind = u32, so swap path is just [0, 1]
const swapPath = [0, 1]; // wHEZ → PEZ
const swapTx = api.tx.assetConversion.swapExactTokensForTokens(
const swapTx = assetHubApi.tx.assetConversion.swapExactTokensForTokens(
swapPath,
amountIn.toString(),
minAmountOut.toString(),
selectedAccount.address,
true
);
tx = api.tx.utility.batchAll([wrapTx, swapTx]);
tx = assetHubApi.tx.utility.batchAll([wrapTx, swapTx]);
} else if (fromToken === 'PEZ' && toToken === 'HEZ') {
// PEZ → HEZ: swap(PEZ→wHEZ) then unwrap(wHEZ→HEZ)
// AssetKind = u32, so swap path is just [1, 0]
const swapPath = [1, 0]; // PEZ → wHEZ
const swapTx = api.tx.assetConversion.swapExactTokensForTokens(
const swapTx = assetHubApi.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]);
const unwrapTx = assetHubApi.tx.tokenWrapper.unwrap(minAmountOut.toString());
tx = assetHubApi.tx.utility.batchAll([swapTx, unwrapTx]);
} else if (fromToken === 'HEZ') {
// HEZ → Any Asset: wrap(HEZ→wHEZ) then swap(wHEZ→Asset)
const wrapTx = api.tx.tokenWrapper.wrap(amountIn.toString());
const wrapTx = assetHubApi.tx.tokenWrapper.wrap(amountIn.toString());
// Map token symbol to asset ID
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 = api.tx.assetConversion.swapExactTokensForTokens(
const swapTx = assetHubApi.tx.assetConversion.swapExactTokensForTokens(
swapPath,
amountIn.toString(),
minAmountOut.toString(),
selectedAccount.address,
true
);
tx = api.tx.utility.batchAll([wrapTx, swapTx]);
tx = assetHubApi.tx.utility.batchAll([wrapTx, swapTx]);
} else if (toToken === 'HEZ') {
// Any Asset → HEZ: swap(Asset→wHEZ) then unwrap(wHEZ→HEZ)
// Map token symbol to asset ID
const fromAssetId = fromToken === 'PEZ' ? 1 : fromToken === 'USDT' ? 1000 : ASSET_IDS[fromToken as keyof typeof ASSET_IDS];
const swapPath = [fromAssetId, 0]; // source asset → wHEZ
const swapTx = api.tx.assetConversion.swapExactTokensForTokens(
const swapTx = assetHubApi.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]);
const unwrapTx = assetHubApi.tx.tokenWrapper.unwrap(minAmountOut.toString());
tx = assetHubApi.tx.utility.batchAll([swapTx, unwrapTx]);
} else {
// Direct swap between assets (PEZ ↔ USDT, etc.)
@@ -614,7 +615,7 @@ const TokenSwap = () => {
const toAssetId = toToken === 'PEZ' ? 1 : toToken === 'USDT' ? 1000 : ASSET_IDS[toToken as keyof typeof ASSET_IDS];
const swapPath = [fromAssetId, toAssetId];
tx = api.tx.assetConversion.swapExactTokensForTokens(
tx = assetHubApi.tx.assetConversion.swapExactTokensForTokens(
swapPath,
amountIn.toString(),
minAmountOut.toString(),
@@ -649,7 +650,7 @@ const TokenSwap = () => {
let errorMessage = 'Transaction failed';
if (dispatchError.isModule) {
const decoded = api.registry.findMetaError(dispatchError.asModule);
const decoded = assetHubApi.registry.findMetaError(dispatchError.asModule);
errorMessage = `${decoded.section}.${decoded.name}: ${decoded.docs}`;
}
@@ -664,7 +665,7 @@ const TokenSwap = () => {
// Success - check for swap event
const hasSwapEvent = events.some(({ event }) =>
api.events.assetConversion?.SwapExecuted?.is(event)
assetHubApi.events.assetConversion?.SwapExecuted?.is(event)
);
if (hasSwapEvent || fromToken === 'HEZ' || toToken === 'HEZ') {
@@ -683,23 +684,23 @@ const TokenSwap = () => {
setTimeout(async () => {
if (import.meta.env.DEV) console.log('🔄 Refreshing swap history...');
const fetchSwapHistory = async () => {
if (!api || !isApiReady || !isDexAvailable || !selectedAccount) return;
if (!assetHubApi || !isAssetHubReady || !isDexAvailable || !selectedAccount) return;
setIsLoadingHistory(true);
try {
const finalizedHead = await api.rpc.chain.getFinalizedHead();
const finalizedBlock = await api.rpc.chain.getBlock(finalizedHead);
const finalizedHead = await assetHubApi.rpc.chain.getFinalizedHead();
const finalizedBlock = await assetHubApi.rpc.chain.getBlock(finalizedHead);
const currentBlockNumber = finalizedBlock.block.header.number.toNumber();
const startBlock = Math.max(0, currentBlockNumber - 100);
const transactions: SwapTransaction[] = [];
for (let blockNum = currentBlockNumber; blockNum >= startBlock && transactions.length < 10; blockNum--) {
try {
const blockHash = await api.rpc.chain.getBlockHash(blockNum);
const apiAt = await api.at(blockHash);
const blockHash = await assetHubApi.rpc.chain.getBlockHash(blockNum);
const apiAt = await assetHubApi.at(blockHash);
const events = await apiAt.query.system.events();
const timestamp = Date.now() - ((currentBlockNumber - blockNum) * 6000);
events.forEach((record: { event: { data: unknown[] } }) => {
const { event } = record;
if (api.events.assetConversion?.SwapExecuted?.is(event)) {
if (assetHubApi.events.assetConversion?.SwapExecuted?.is(event)) {
// SwapExecuted has 5 fields: (who, send_to, amountIn, amountOut, path)
const [who, , amountIn, amountOut, path] = event.data;
@@ -780,7 +781,7 @@ const TokenSwap = () => {
};
// Show DEX unavailable message
if (!isDexAvailable && isApiReady) {
if (!isDexAvailable && isAssetHubReady) {
return (
<div className="max-w-4xl mx-auto">
<Card className="p-8">
@@ -21,7 +21,8 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
onClose,
onSuccess,
}) => {
const { api, isApiReady } = usePezkuwi();
// Use Asset Hub API for DEX operations (assetConversion pallet is on Asset Hub)
const { assetHubApi, isAssetHubReady } = usePezkuwi();
const { account, signer } = useWallet();
const [lpTokenBalance, setLpTokenBalance] = useState<string>('0');
@@ -43,11 +44,11 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
// Fetch LP token balance
useEffect(() => {
const fetchLPBalance = async () => {
if (!api || !isApiReady || !account || !pool) return;
if (!assetHubApi || !isAssetHubReady || !account || !pool) return;
try {
// Get pool account
const poolAccount = await api.query.assetConversion.pools([
const poolAccount = await assetHubApi.query.assetConversion.pools([
pool.asset1,
pool.asset2,
]);
@@ -60,8 +61,8 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
// LP token ID is derived from pool ID
// For now, we'll query the pool's LP token supply
// In a real implementation, you'd need to query the specific LP token for the user
if (api.query.assetConversion.nextPoolAssetId) {
await api.query.assetConversion.nextPoolAssetId();
if (assetHubApi.query.assetConversion.nextPoolAssetId) {
await assetHubApi.query.assetConversion.nextPoolAssetId();
}
// This is a simplified version - you'd need to track LP tokens properly
@@ -73,7 +74,7 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
};
fetchLPBalance();
}, [api, isApiReady, account, pool]);
}, [assetHubApi, isAssetHubReady, account, pool]);
const calculateOutputAmounts = () => {
if (!pool || BigInt(lpTokenBalance) === BigInt(0)) {
@@ -98,7 +99,7 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
};
const handleRemoveLiquidity = async () => {
if (!api || !isApiReady || !signer || !account || !pool) {
if (!assetHubApi || !isAssetHubReady || !signer || !account || !pool) {
setErrorMessage('Wallet not connected');
return;
}
@@ -119,7 +120,7 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
setTxStatus('signing');
setErrorMessage('');
const tx = api.tx.assetConversion.removeLiquidity(
const tx = assetHubApi.tx.assetConversion.removeLiquidity(
pool.asset1,
pool.asset2,
lpAmount.toString(),
@@ -137,7 +138,7 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
if (status.isInBlock) {
if (dispatchError) {
if (dispatchError.isModule) {
const decoded = api.registry.findMetaError(dispatchError.asModule);
const decoded = assetHubApi.registry.findMetaError(dispatchError.asModule);
setErrorMessage(`${decoded.section}.${decoded.name}: ${decoded.docs}`);
} else {
setErrorMessage(dispatchError.toString());