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 {estimateExecuteSwapOrderGasLimit} from '../../generated/gmxsdk/utils/fees/executionFee.js';
|
||||||
import {getExecutionFee} 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 {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 {
|
import {
|
||||||
Position,
|
Position,
|
||||||
PositionStatus,
|
PositionStatus,
|
||||||
@@ -2949,14 +2952,36 @@ export const swapGmxTokensImpl = async (
|
|||||||
toTicker,
|
toTicker,
|
||||||
fromTokenAddress: fromTokenData?.address,
|
fromTokenAddress: fromTokenData?.address,
|
||||||
fromTokenIsNative: fromTokenData?.isNative,
|
fromTokenIsNative: fromTokenData?.isNative,
|
||||||
|
fromTokenIsSynthetic: fromTokenData?.isSynthetic,
|
||||||
toTokenAddress: toTokenData?.address,
|
toTokenAddress: toTokenData?.address,
|
||||||
toTokenIsNative: toTokenData?.isNative
|
toTokenIsNative: toTokenData?.isNative,
|
||||||
|
toTokenIsSynthetic: toTokenData?.isSynthetic
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!fromTokenData || !toTokenData) {
|
if (!fromTokenData || !toTokenData) {
|
||||||
throw new Error(`Token data not found for ${fromTicker} or ${toTicker}`);
|
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)
|
// Check for zero addresses (but allow native tokens like ETH)
|
||||||
if (fromTokenData.address === "0x0000000000000000000000000000000000000000" && !fromTokenData.isNative) {
|
if (fromTokenData.address === "0x0000000000000000000000000000000000000000" && !fromTokenData.isNative) {
|
||||||
throw new Error(`From token ${fromTicker} has zero address - token not found or invalid`);
|
throw new Error(`From token ${fromTicker} has zero address - token not found or invalid`);
|
||||||
@@ -3122,44 +3147,95 @@ export const swapGmxTokensImpl = async (
|
|||||||
toTicker
|
toTicker
|
||||||
});
|
});
|
||||||
|
|
||||||
// Try using the SDK's built-in swap method first
|
// Check if either token is synthetic - SDK swap doesn't support synthetic tokens
|
||||||
try {
|
const hasSyntheticToken = fromTokenData.isSynthetic || toTokenData.isSynthetic;
|
||||||
const swapParams = {
|
|
||||||
fromAmount: verifiedFromTokenAmount,
|
if (hasSyntheticToken) {
|
||||||
fromTokenAddress: fromTokenData.address,
|
console.log('🔄 Synthetic token detected, using direct createSwapOrderTxn (SDK swap does not support synthetic tokens):', {
|
||||||
toTokenAddress: toTokenData.address,
|
fromToken: fromTokenData.symbol,
|
||||||
allowedSlippageBps: allowedSlippageBps,
|
fromIsSynthetic: fromTokenData.isSynthetic,
|
||||||
referralCodeForTxn: encodeReferralCode("kaigen_ai"),
|
toToken: toTokenData.symbol,
|
||||||
skipSimulation: true,
|
toIsSynthetic: toTokenData.isSynthetic
|
||||||
} as SwapParams;
|
|
||||||
|
|
||||||
swapParams.marketsInfoData = marketsInfoData;
|
|
||||||
swapParams.tokensData = tokensData;
|
|
||||||
|
|
||||||
console.log('🔄 Attempting SDK swap with slippage:', {
|
|
||||||
allowedSlippageBps: allowedSlippageBps,
|
|
||||||
slippagePercentage: slippagePercentage
|
|
||||||
});
|
});
|
||||||
await sdk.orders.swap(swapParams);
|
} else {
|
||||||
console.log('✅ SDK swap successful!');
|
// Try using the SDK's built-in swap method first (only for non-synthetic tokens)
|
||||||
} catch (sdkError) {
|
try {
|
||||||
console.log('❌ SDK swap failed, trying alternative approach:', sdkError);
|
const swapParams = {
|
||||||
|
fromAmount: verifiedFromTokenAmount,
|
||||||
// Fall back to createSwapOrderTxn directly with simpler parameters
|
fromTokenAddress: fromTokenData.address,
|
||||||
console.log('🔄 Attempting direct createSwapOrderTxn...');
|
toTokenAddress: toTokenData.address,
|
||||||
|
allowedSlippageBps: allowedSlippageBps,
|
||||||
// Calculate execution fee dynamically
|
referralCodeForTxn: encodeReferralCode("kaigen_ai"),
|
||||||
const swapPath = [fromTokenData.address, toTokenData.address];
|
skipSimulation: true,
|
||||||
// For a direct swap (token A -> token B), there's 1 swap through 1 market
|
} as SwapParams;
|
||||||
// The swapPath length represents the number of markets/swaps in the path
|
|
||||||
// For a simple direct swap, we estimate 1 swap
|
swapParams.marketsInfoData = marketsInfoData;
|
||||||
const swapsCount = 1;
|
swapParams.tokensData = tokensData;
|
||||||
const gasLimits = await sdk.utils.getGasLimits();
|
|
||||||
const gasPrice = await sdk.utils.getGasPrice();
|
console.log('🔄 Attempting SDK swap with slippage:', {
|
||||||
|
allowedSlippageBps: allowedSlippageBps,
|
||||||
if (!gasLimits || gasPrice === undefined) {
|
slippagePercentage: slippagePercentage
|
||||||
throw new Error("Failed to get gas limits or gas price for execution fee calculation");
|
});
|
||||||
|
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, {
|
const estimatedGasLimit = estimateExecuteSwapOrderGasLimit(gasLimits, {
|
||||||
swapsCount: swapsCount,
|
swapsCount: swapsCount,
|
||||||
@@ -3183,32 +3259,43 @@ export const swapGmxTokensImpl = async (
|
|||||||
|
|
||||||
console.log(`💰 Calculated execution fee: ${(Number(executionFee.feeTokenAmount) / 1e18).toFixed(6)} ETH`);
|
console.log(`💰 Calculated execution fee: ${(Number(executionFee.feeTokenAmount) / 1e18).toFixed(6)} ETH`);
|
||||||
|
|
||||||
// Calculate minOutputAmount based on current market price and slippage
|
// Calculate minOutputAmount based on swap path stats or fallback to price-based calculation
|
||||||
// Get the current price ratio from token data
|
let minOutputAmount: bigint;
|
||||||
const fromTokenPrice = fromTokenData.prices?.minPrice
|
|
||||||
? Number(fromTokenData.prices.minPrice) / 1e30
|
|
||||||
: 1;
|
|
||||||
const toTokenPrice = toTokenData.prices?.minPrice
|
|
||||||
? Number(toTokenData.prices.minPrice) / 1e30
|
|
||||||
: 1;
|
|
||||||
|
|
||||||
// Calculate expected output amount (simplified - assumes 1:1 price ratio if prices unavailable)
|
if (swapPathStats && swapPathStats.amountOut) {
|
||||||
const fromAmountInUsd = (Number(verifiedFromTokenAmount) / Math.pow(10, fromTokenData.decimals)) * fromTokenPrice;
|
// Use the amount from swap path stats (more accurate)
|
||||||
const expectedOutputInUsd = fromAmountInUsd * (toTokenPrice / fromTokenPrice);
|
const slippageMultiplier = BigInt(10000 - allowedSlippageBps); // e.g., 9950 for 0.5% slippage
|
||||||
const expectedOutputInTokens = expectedOutputInUsd / toTokenPrice;
|
minOutputAmount = (swapPathStats.amountOut * slippageMultiplier) / 10000n;
|
||||||
|
console.log('💱 Using swap path stats for minOutputAmount:', {
|
||||||
// Apply slippage tolerance to minimum output
|
amountOut: swapPathStats.amountOut.toString(),
|
||||||
const minOutputAmount = BigInt(Math.floor(
|
minOutputAmount: minOutputAmount.toString(),
|
||||||
expectedOutputInTokens * Math.pow(10, toTokenData.decimals) * (1 - slippagePercentage / 100)
|
slippageBps: allowedSlippageBps
|
||||||
));
|
});
|
||||||
|
} else {
|
||||||
console.log('💱 Swap output calculation:', {
|
// Fallback to price-based calculation
|
||||||
fromAmountUsd: fromAmountInUsd.toFixed(2),
|
const fromTokenPrice = fromTokenData.prices?.minPrice
|
||||||
expectedOutputUsd: expectedOutputInUsd.toFixed(2),
|
? Number(fromTokenData.prices.minPrice) / 1e30
|
||||||
expectedOutputTokens: expectedOutputInTokens.toFixed(6),
|
: 1;
|
||||||
minOutputAmount: minOutputAmount.toString(),
|
const toTokenPrice = toTokenData.prices?.minPrice
|
||||||
slippagePercentage: slippagePercentage
|
? Number(toTokenData.prices.minPrice) / 1e30
|
||||||
});
|
: 1;
|
||||||
|
|
||||||
|
const fromAmountInUsd = (Number(verifiedFromTokenAmount) / Math.pow(10, fromTokenData.decimals)) * fromTokenPrice;
|
||||||
|
const expectedOutputInUsd = fromAmountInUsd * (toTokenPrice / fromTokenPrice);
|
||||||
|
const expectedOutputInTokens = expectedOutputInUsd / toTokenPrice;
|
||||||
|
|
||||||
|
// 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, {
|
await createSwapOrderTxn(sdk, {
|
||||||
fromTokenAddress: fromTokenData.address,
|
fromTokenAddress: fromTokenData.address,
|
||||||
@@ -3224,7 +3311,6 @@ export const swapGmxTokensImpl = async (
|
|||||||
skipSimulation: true,
|
skipSimulation: true,
|
||||||
});
|
});
|
||||||
console.log('✅ Direct createSwapOrderTxn successful!');
|
console.log('✅ Direct createSwapOrderTxn successful!');
|
||||||
}
|
|
||||||
|
|
||||||
return "swap_order_created";
|
return "swap_order_created";
|
||||||
}, sdk, 0
|
}, sdk, 0
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ describe('swap tokens implementation', () => {
|
|||||||
|
|
||||||
const result = await swapGmxTokensImpl(
|
const result = await swapGmxTokensImpl(
|
||||||
sdk,
|
sdk,
|
||||||
Ticker.SOL,
|
|
||||||
Ticker.USDC,
|
Ticker.USDC,
|
||||||
0.0495,
|
Ticker.RENDER,
|
||||||
|
5, // 5 USDC
|
||||||
'market',
|
'market',
|
||||||
undefined,
|
undefined,
|
||||||
0.5
|
0.5
|
||||||
|
|||||||
Reference in New Issue
Block a user