Implement synthetic token validation and enhance swap logic in GMX plugin
- Added early validation to prevent swapping synthetic tokens, ensuring users are informed about the limitations of synthetic tokens. - Enhanced the swap logic to handle synthetic tokens by falling back to a direct swap order transaction when synthetic tokens are involved or when the SDK swap fails. - Improved the calculation of minimum output amounts based on swap path statistics or fallback to price-based calculations for better accuracy.
This commit is contained in:
@@ -35,6 +35,9 @@ import {approveContractImpl, getTokenAllowance} from './privy.js';
|
||||
import {estimateExecuteSwapOrderGasLimit} from '../../generated/gmxsdk/utils/fees/executionFee.js';
|
||||
import {getExecutionFee} from '../../generated/gmxsdk/utils/fees/executionFee.js';
|
||||
import {estimateOrderOraclePriceCount} from '../../generated/gmxsdk/utils/fees/estimateOraclePriceCount.js';
|
||||
import {createFindSwapPath} from '../../generated/gmxsdk/utils/swap/swapPath.js';
|
||||
import {SwapPricingType} from '../../generated/gmxsdk/types/orders.js';
|
||||
import {convertToUsd} from '../../generated/gmxsdk/utils/tokens.js';
|
||||
import {
|
||||
Position,
|
||||
PositionStatus,
|
||||
@@ -2949,14 +2952,36 @@ export const swapGmxTokensImpl = async (
|
||||
toTicker,
|
||||
fromTokenAddress: fromTokenData?.address,
|
||||
fromTokenIsNative: fromTokenData?.isNative,
|
||||
fromTokenIsSynthetic: fromTokenData?.isSynthetic,
|
||||
toTokenAddress: toTokenData?.address,
|
||||
toTokenIsNative: toTokenData?.isNative
|
||||
toTokenIsNative: toTokenData?.isNative,
|
||||
toTokenIsSynthetic: toTokenData?.isSynthetic
|
||||
});
|
||||
|
||||
if (!fromTokenData || !toTokenData) {
|
||||
throw new Error(`Token data not found for ${fromTicker} or ${toTicker}`);
|
||||
}
|
||||
|
||||
// EARLY VALIDATION: Check for synthetic tokens before any transaction preparation
|
||||
// Synthetic tokens cannot be swapped to/from - they can only be obtained through positions
|
||||
if (toTokenData.isSynthetic) {
|
||||
const marketConfig = getMarketByIndexToken(toTokenData.address);
|
||||
const marketInfo = marketConfig ? `Market: ${marketConfig.marketTokenAddress}` : 'No market found';
|
||||
throw new Error(
|
||||
`Cannot swap to synthetic token ${toTokenData.symbol}. ` +
|
||||
`Synthetic tokens are index tokens and can only be obtained by opening a long position. ` +
|
||||
`Please use the open-position endpoint instead. ${marketInfo}`
|
||||
);
|
||||
}
|
||||
|
||||
if (fromTokenData.isSynthetic) {
|
||||
throw new Error(
|
||||
`Cannot swap from synthetic token ${fromTokenData.symbol}. ` +
|
||||
`Synthetic tokens cannot be used as input for swaps. ` +
|
||||
`Please close your position first to convert the synthetic token to collateral.`
|
||||
);
|
||||
}
|
||||
|
||||
// Check for zero addresses (but allow native tokens like ETH)
|
||||
if (fromTokenData.address === "0x0000000000000000000000000000000000000000" && !fromTokenData.isNative) {
|
||||
throw new Error(`From token ${fromTicker} has zero address - token not found or invalid`);
|
||||
@@ -3122,44 +3147,95 @@ export const swapGmxTokensImpl = async (
|
||||
toTicker
|
||||
});
|
||||
|
||||
// Try using the SDK's built-in swap method first
|
||||
try {
|
||||
const swapParams = {
|
||||
fromAmount: verifiedFromTokenAmount,
|
||||
fromTokenAddress: fromTokenData.address,
|
||||
toTokenAddress: toTokenData.address,
|
||||
allowedSlippageBps: allowedSlippageBps,
|
||||
referralCodeForTxn: encodeReferralCode("kaigen_ai"),
|
||||
skipSimulation: true,
|
||||
} as SwapParams;
|
||||
// Check if either token is synthetic - SDK swap doesn't support synthetic tokens
|
||||
const hasSyntheticToken = fromTokenData.isSynthetic || toTokenData.isSynthetic;
|
||||
|
||||
swapParams.marketsInfoData = marketsInfoData;
|
||||
swapParams.tokensData = tokensData;
|
||||
|
||||
console.log('🔄 Attempting SDK swap with slippage:', {
|
||||
allowedSlippageBps: allowedSlippageBps,
|
||||
slippagePercentage: slippagePercentage
|
||||
if (hasSyntheticToken) {
|
||||
console.log('🔄 Synthetic token detected, using direct createSwapOrderTxn (SDK swap does not support synthetic tokens):', {
|
||||
fromToken: fromTokenData.symbol,
|
||||
fromIsSynthetic: fromTokenData.isSynthetic,
|
||||
toToken: toTokenData.symbol,
|
||||
toIsSynthetic: toTokenData.isSynthetic
|
||||
});
|
||||
await sdk.orders.swap(swapParams);
|
||||
console.log('✅ SDK swap successful!');
|
||||
} catch (sdkError) {
|
||||
console.log('❌ SDK swap failed, trying alternative approach:', sdkError);
|
||||
} else {
|
||||
// Try using the SDK's built-in swap method first (only for non-synthetic tokens)
|
||||
try {
|
||||
const swapParams = {
|
||||
fromAmount: verifiedFromTokenAmount,
|
||||
fromTokenAddress: fromTokenData.address,
|
||||
toTokenAddress: toTokenData.address,
|
||||
allowedSlippageBps: allowedSlippageBps,
|
||||
referralCodeForTxn: encodeReferralCode("kaigen_ai"),
|
||||
skipSimulation: true,
|
||||
} as SwapParams;
|
||||
|
||||
// Fall back to createSwapOrderTxn directly with simpler parameters
|
||||
console.log('🔄 Attempting direct createSwapOrderTxn...');
|
||||
swapParams.marketsInfoData = marketsInfoData;
|
||||
swapParams.tokensData = tokensData;
|
||||
|
||||
// Calculate execution fee dynamically
|
||||
const swapPath = [fromTokenData.address, toTokenData.address];
|
||||
// For a direct swap (token A -> token B), there's 1 swap through 1 market
|
||||
// The swapPath length represents the number of markets/swaps in the path
|
||||
// For a simple direct swap, we estimate 1 swap
|
||||
const swapsCount = 1;
|
||||
const gasLimits = await sdk.utils.getGasLimits();
|
||||
const gasPrice = await sdk.utils.getGasPrice();
|
||||
|
||||
if (!gasLimits || gasPrice === undefined) {
|
||||
throw new Error("Failed to get gas limits or gas price for execution fee calculation");
|
||||
console.log('🔄 Attempting SDK swap with slippage:', {
|
||||
allowedSlippageBps: allowedSlippageBps,
|
||||
slippagePercentage: slippagePercentage
|
||||
});
|
||||
await sdk.orders.swap(swapParams);
|
||||
console.log('✅ SDK swap successful!');
|
||||
return "swap_order_created";
|
||||
} catch (sdkError) {
|
||||
console.log('❌ SDK swap failed, trying alternative approach:', sdkError);
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to createSwapOrderTxn directly (for synthetic tokens or if SDK swap fails)
|
||||
console.log('🔄 Attempting direct createSwapOrderTxn...');
|
||||
|
||||
// Get gas limits and price first (needed for swap path finding)
|
||||
const gasLimits = await sdk.utils.getGasLimits();
|
||||
const gasPrice = await sdk.utils.getGasPrice();
|
||||
|
||||
if (!gasLimits || gasPrice === undefined) {
|
||||
throw new Error("Failed to get gas limits or gas price for execution fee calculation");
|
||||
}
|
||||
|
||||
// Find the proper swap path using createFindSwapPath (works for both synthetic and non-synthetic tokens)
|
||||
const findSwapPath = createFindSwapPath({
|
||||
chainId: sdk.chainId,
|
||||
fromTokenAddress: fromTokenData.address,
|
||||
toTokenAddress: toTokenData.address,
|
||||
marketsInfoData,
|
||||
gasEstimationParams: {
|
||||
gasLimits,
|
||||
gasPrice,
|
||||
tokensData,
|
||||
},
|
||||
swapPricingType: SwapPricingType.Swap,
|
||||
});
|
||||
|
||||
// Calculate USD amount for swap path finding
|
||||
const fromTokenPrice = fromTokenData.prices?.minPrice
|
||||
? Number(fromTokenData.prices.minPrice) / 1e30
|
||||
: 1;
|
||||
const usdIn = convertToUsd(verifiedFromTokenAmount, fromTokenData.decimals, BigInt(Math.floor(fromTokenPrice * 1e30))) || 0n;
|
||||
|
||||
// Find the swap path
|
||||
const swapPathStats = findSwapPath(usdIn);
|
||||
let swapPath: string[];
|
||||
let swapsCount: number;
|
||||
|
||||
if (swapPathStats && swapPathStats.swapPath) {
|
||||
swapPath = swapPathStats.swapPath;
|
||||
swapsCount = swapPath.length - 1; // Number of swaps = path length - 1
|
||||
console.log('✅ Found swap path:', {
|
||||
path: swapPath,
|
||||
swapsCount,
|
||||
usdOut: swapPathStats.usdOut?.toString(),
|
||||
amountOut: swapPathStats.amountOut?.toString()
|
||||
});
|
||||
} else {
|
||||
// No swap path found - this should not happen for valid token pairs
|
||||
// (Synthetic tokens are already validated early, so we won't reach here for them)
|
||||
console.warn('⚠️ No swap path found, using direct path (may not work for all token pairs)');
|
||||
swapPath = [fromTokenData.address, toTokenData.address];
|
||||
swapsCount = 1;
|
||||
}
|
||||
|
||||
const estimatedGasLimit = estimateExecuteSwapOrderGasLimit(gasLimits, {
|
||||
swapsCount: swapsCount,
|
||||
@@ -3183,32 +3259,43 @@ export const swapGmxTokensImpl = async (
|
||||
|
||||
console.log(`💰 Calculated execution fee: ${(Number(executionFee.feeTokenAmount) / 1e18).toFixed(6)} ETH`);
|
||||
|
||||
// Calculate minOutputAmount based on current market price and slippage
|
||||
// Get the current price ratio from token data
|
||||
const fromTokenPrice = fromTokenData.prices?.minPrice
|
||||
? Number(fromTokenData.prices.minPrice) / 1e30
|
||||
: 1;
|
||||
const toTokenPrice = toTokenData.prices?.minPrice
|
||||
? Number(toTokenData.prices.minPrice) / 1e30
|
||||
: 1;
|
||||
// Calculate minOutputAmount based on swap path stats or fallback to price-based calculation
|
||||
let minOutputAmount: bigint;
|
||||
|
||||
// Calculate expected output amount (simplified - assumes 1:1 price ratio if prices unavailable)
|
||||
const fromAmountInUsd = (Number(verifiedFromTokenAmount) / Math.pow(10, fromTokenData.decimals)) * fromTokenPrice;
|
||||
const expectedOutputInUsd = fromAmountInUsd * (toTokenPrice / fromTokenPrice);
|
||||
const expectedOutputInTokens = expectedOutputInUsd / toTokenPrice;
|
||||
if (swapPathStats && swapPathStats.amountOut) {
|
||||
// Use the amount from swap path stats (more accurate)
|
||||
const slippageMultiplier = BigInt(10000 - allowedSlippageBps); // e.g., 9950 for 0.5% slippage
|
||||
minOutputAmount = (swapPathStats.amountOut * slippageMultiplier) / 10000n;
|
||||
console.log('💱 Using swap path stats for minOutputAmount:', {
|
||||
amountOut: swapPathStats.amountOut.toString(),
|
||||
minOutputAmount: minOutputAmount.toString(),
|
||||
slippageBps: allowedSlippageBps
|
||||
});
|
||||
} else {
|
||||
// Fallback to price-based calculation
|
||||
const fromTokenPrice = fromTokenData.prices?.minPrice
|
||||
? Number(fromTokenData.prices.minPrice) / 1e30
|
||||
: 1;
|
||||
const toTokenPrice = toTokenData.prices?.minPrice
|
||||
? Number(toTokenData.prices.minPrice) / 1e30
|
||||
: 1;
|
||||
|
||||
// Apply slippage tolerance to minimum output
|
||||
const minOutputAmount = BigInt(Math.floor(
|
||||
expectedOutputInTokens * Math.pow(10, toTokenData.decimals) * (1 - slippagePercentage / 100)
|
||||
));
|
||||
const fromAmountInUsd = (Number(verifiedFromTokenAmount) / Math.pow(10, fromTokenData.decimals)) * fromTokenPrice;
|
||||
const expectedOutputInUsd = fromAmountInUsd * (toTokenPrice / fromTokenPrice);
|
||||
const expectedOutputInTokens = expectedOutputInUsd / toTokenPrice;
|
||||
|
||||
console.log('💱 Swap output calculation:', {
|
||||
fromAmountUsd: fromAmountInUsd.toFixed(2),
|
||||
expectedOutputUsd: expectedOutputInUsd.toFixed(2),
|
||||
expectedOutputTokens: expectedOutputInTokens.toFixed(6),
|
||||
minOutputAmount: minOutputAmount.toString(),
|
||||
slippagePercentage: slippagePercentage
|
||||
});
|
||||
// Apply slippage tolerance to minimum output
|
||||
minOutputAmount = BigInt(Math.floor(
|
||||
expectedOutputInTokens * Math.pow(10, toTokenData.decimals) * (1 - slippagePercentage / 100)
|
||||
));
|
||||
console.log('💱 Using price-based calculation for minOutputAmount (fallback):', {
|
||||
fromAmountUsd: fromAmountInUsd.toFixed(2),
|
||||
expectedOutputUsd: expectedOutputInUsd.toFixed(2),
|
||||
expectedOutputTokens: expectedOutputInTokens.toFixed(6),
|
||||
minOutputAmount: minOutputAmount.toString(),
|
||||
slippagePercentage: slippagePercentage
|
||||
});
|
||||
}
|
||||
|
||||
await createSwapOrderTxn(sdk, {
|
||||
fromTokenAddress: fromTokenData.address,
|
||||
@@ -3224,7 +3311,6 @@ export const swapGmxTokensImpl = async (
|
||||
skipSimulation: true,
|
||||
});
|
||||
console.log('✅ Direct createSwapOrderTxn successful!');
|
||||
}
|
||||
|
||||
return "swap_order_created";
|
||||
}, sdk, 0
|
||||
|
||||
@@ -13,9 +13,9 @@ describe('swap tokens implementation', () => {
|
||||
|
||||
const result = await swapGmxTokensImpl(
|
||||
sdk,
|
||||
Ticker.SOL,
|
||||
Ticker.USDC,
|
||||
0.0495,
|
||||
Ticker.RENDER,
|
||||
5, // 5 USDC
|
||||
'market',
|
||||
undefined,
|
||||
0.5
|
||||
|
||||
Reference in New Issue
Block a user