diff --git a/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts b/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts index 4a7474d5..6bbb322e 100644 --- a/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts +++ b/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts @@ -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; - - swapParams.marketsInfoData = marketsInfoData; - swapParams.tokensData = tokensData; - - console.log('🔄 Attempting SDK swap with slippage:', { - allowedSlippageBps: allowedSlippageBps, - slippagePercentage: slippagePercentage + // Check if either token is synthetic - SDK swap doesn't support synthetic tokens + const hasSyntheticToken = fromTokenData.isSynthetic || toTokenData.isSynthetic; + + 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); - - // Fall back to createSwapOrderTxn directly with simpler parameters - console.log('🔄 Attempting direct createSwapOrderTxn...'); - - // 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"); + } 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; + + swapParams.marketsInfoData = marketsInfoData; + swapParams.tokensData = tokensData; + + 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; - - // Apply slippage tolerance to minimum output - const minOutputAmount = BigInt(Math.floor( - expectedOutputInTokens * Math.pow(10, toTokenData.decimals) * (1 - slippagePercentage / 100) - )); - - console.log('💱 Swap output calculation:', { - fromAmountUsd: fromAmountInUsd.toFixed(2), - expectedOutputUsd: expectedOutputInUsd.toFixed(2), - expectedOutputTokens: expectedOutputInTokens.toFixed(6), - minOutputAmount: minOutputAmount.toString(), - slippagePercentage: slippagePercentage - }); + 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; + + 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, { fromTokenAddress: fromTokenData.address, @@ -3224,7 +3311,6 @@ export const swapGmxTokensImpl = async ( skipSimulation: true, }); console.log('✅ Direct createSwapOrderTxn successful!'); - } return "swap_order_created"; }, sdk, 0 diff --git a/src/Managing.Web3Proxy/test/plugins/swap-tokens.test.ts b/src/Managing.Web3Proxy/test/plugins/swap-tokens.test.ts index 595950aa..bf9a59b0 100644 --- a/src/Managing.Web3Proxy/test/plugins/swap-tokens.test.ts +++ b/src/Managing.Web3Proxy/test/plugins/swap-tokens.test.ts @@ -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