diff --git a/src/Managing.Web3Proxy/src/generated/gmxsdk/configs/contracts.ts b/src/Managing.Web3Proxy/src/generated/gmxsdk/configs/contracts.ts index 029db0c..94703d3 100644 --- a/src/Managing.Web3Proxy/src/generated/gmxsdk/configs/contracts.ts +++ b/src/Managing.Web3Proxy/src/generated/gmxsdk/configs/contracts.ts @@ -115,7 +115,7 @@ export const CONTRACTS = { DataStore: "0xFD70de6b91282D8017aA4E741e9Ae325CAb992d8", EventEmitter: "0xC8ee91A54287DB53897056e12D9819156D3822Fb", SubaccountRouter: "0xa329221a77BE08485f59310b873b14815c82E10D", - ExchangeRouter: "0x5ac4e27341e4cccb3e5fd62f9e62db2adf43dd57", + ExchangeRouter: "0x602b805EedddBbD9ddff44A7dcBD46cb07849685", DepositVault: "0xF89e77e8Dc11691C9e8757e84aaFbCD8A67d7A55", WithdrawalVault: "0x0628D46b5D145f183AdB6Ef1f2c97eD1C4701C55", OrderVault: "0x31eF83a530Fde1B38EE9A18093A333D8Bbbc40D5", diff --git a/src/Managing.Web3Proxy/src/generated/gmxsdk/modules/orders/transactions/createSwapOrderTxn.ts b/src/Managing.Web3Proxy/src/generated/gmxsdk/modules/orders/transactions/createSwapOrderTxn.ts index 700b01b..35fc6d7 100644 --- a/src/Managing.Web3Proxy/src/generated/gmxsdk/modules/orders/transactions/createSwapOrderTxn.ts +++ b/src/Managing.Web3Proxy/src/generated/gmxsdk/modules/orders/transactions/createSwapOrderTxn.ts @@ -1,17 +1,18 @@ -import { Abi, encodeFunctionData, zeroAddress, zeroHash } from "viem"; +import {Abi, encodeFunctionData, zeroAddress, zeroHash} from "viem"; -import { abis } from "../../../abis/index.js"; -import { getContract } from "../../../configs/contracts.js"; -import { NATIVE_TOKEN_ADDRESS, convertTokenAddress } from "../../../configs/tokens.js"; +import {abis} from "../../../abis/index.js"; +import {getContract} from "../../../configs/contracts.js"; +import {convertTokenAddress, NATIVE_TOKEN_ADDRESS} from "../../../configs/tokens.js"; -import { DecreasePositionSwapType, OrderType } from "../../../types/orders.js"; -import { TokensData } from "../../../types/tokens.js"; +import {DecreasePositionSwapType, OrderType} from "../../../types/orders.js"; +import {TokensData} from "../../../types/tokens.js"; -import { isMarketOrderType } from "../../../utils/orders.js"; -import { applySlippageToMinOut } from "../../../utils/trade/index.js"; -import { simulateExecuteOrder } from "../../../utils/simulateExecuteOrder.js"; +import {isMarketOrderType} from "../../../utils/orders.js"; +import {applySlippageToMinOut} from "../../../utils/trade/index.js"; +import {simulateExecuteOrder} from "../../../utils/simulateExecuteOrder.js"; -import type { GmxSdk } from "../../.."; +import type {GmxSdk} from "../../.."; +import {DEFAULT_UI_FEE_RECEIVER_ACCOUNT} from "../../utils/utils.js"; export type SwapOrderParams = { fromTokenAddress: string; @@ -31,7 +32,8 @@ export async function createSwapOrderTxn(sdk: GmxSdk, p: SwapOrderParams) { const { encodedPayload, totalWntAmount } = await getParams(sdk, p); const { encodedPayload: simulationEncodedPayload, totalWntAmount: sumaltionTotalWntAmount } = await getParams(sdk, p); - if (p.orderType !== OrderType.LimitSwap) { + const skipSimulation = true; + if (p.orderType !== OrderType.LimitSwap && skipSimulation !== true) { await simulateExecuteOrder(sdk, { primaryPriceOverrides: {}, createMulticallPayload: simulationEncodedPayload, @@ -76,7 +78,7 @@ async function getParams(sdk: GmxSdk, p: SwapOrderParams) { callbackContract: zeroAddress, market: zeroAddress, swapPath: p.swapPath, - uiFeeReceiver: zeroAddress, + uiFeeReceiver: DEFAULT_UI_FEE_RECEIVER_ACCOUNT, }, numbers: { sizeDeltaUsd: 0n, diff --git a/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts b/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts index c58f679..6d1567e 100644 --- a/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts +++ b/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts @@ -23,7 +23,7 @@ import {ARBITRUM} from '../../generated/gmxsdk/configs/chains.js' import {TokenData, TokensData} from '../../generated/gmxsdk/types/tokens.js'; import {getByKey} from '../../generated/gmxsdk/utils/objects.js'; import {GmxSdkConfig} from '../../generated/gmxsdk/types/sdk.js'; -import {PositionIncreaseParams} from '../../generated/gmxsdk/modules/orders/helpers.js'; +import {PositionIncreaseParams, SwapParams} from '../../generated/gmxsdk/modules/orders/helpers.js'; import { basisPointsToFloat, bigintToNumber, @@ -42,6 +42,7 @@ import {hashDataMap, hashString} from '../../generated/gmxsdk/utils/hash.js'; import {CLAIMABLE_FUNDING_AMOUNT} from '../../generated/gmxsdk/configs/dataStore.js'; import {abis} from '../../generated/gmxsdk/abis/index.js'; import {DEFAULT_UI_FEE_RECEIVER_ACCOUNT} from '../../generated/gmxsdk/modules/utils/utils.js' +import {approveContractImpl, getTokenAllowance} from './privy.js'; // Add the missing CLAIMABLE_UI_FEE_AMOUNT constant based on the pattern export const CLAIMABLE_UI_FEE_AMOUNT = hashString("CLAIMABLE_UI_FEE_AMOUNT"); @@ -146,6 +147,8 @@ declare module 'fastify' { getGmxPriceImpactRebates: typeof getGmxPriceImpactRebates; getClaimableUiFees: typeof getClaimableUiFees; claimGmxUiFees: typeof claimGmxUiFees; + swapGmxTokens: typeof swapGmxTokens; + checkGmxTokenAllowances: typeof checkGmxTokenAllowances; } } @@ -191,6 +194,20 @@ const getClaimableUiFeesSchema = z.object({ account: z.string().nonempty() }); +// Schema for swap-tokens request +const swapTokensSchema = z.object({ + account: z.string().nonempty(), + fromTicker: z.string().nonempty(), + toTicker: z.string().nonempty(), + amount: z.number().positive(), + orderType: z.enum(['market', 'limit']).default('market'), + triggerRatio: z.number().optional(), + allowedSlippage: z.number().min(0).max(100).default(0.5) +}).refine((data) => data.fromTicker !== data.toTicker, { + message: "From and to tickers must be different", + path: ["toTicker"] +}); + /** * Gets a GMX SDK client initialized for the given address * If a walletId is provided, it will be used with Privy for signing @@ -938,6 +955,8 @@ export default fp(async (fastify) => { fastify.decorateRequest('getGmxPriceImpactRebates', getGmxPriceImpactRebates) fastify.decorateRequest('getClaimableUiFees', getClaimableUiFees) fastify.decorateRequest('claimGmxUiFees', claimGmxUiFees) + fastify.decorateRequest('swapGmxTokens', swapGmxTokens) + fastify.decorateRequest('checkGmxTokenAllowances', checkGmxTokenAllowances) // Pre-populate and refresh the markets cache on startup fastify.addHook('onReady', async () => { @@ -1770,4 +1789,229 @@ export async function claimGmxUiFees( return handleError(this, reply, error, 'gmx/claim-ui-fees'); } } + +/** + * Implementation function to swap tokens on GMX using the SDK's built-in swap method + * @param sdk The GMX SDK client + * @param fromTicker The ticker symbol of the token to swap from + * @param toTicker The ticker symbol of the token to swap to + * @param amount The amount to swap + * @param orderType The order type (market or limit) + * @param triggerRatio The trigger ratio for limit orders (optional) + * @param allowedSlippage The allowed slippage percentage (default 0.5%) + * @returns The transaction hash + */ +export const swapGmxTokensImpl = async ( + sdk: GmxSdk, + fromTicker: string, + toTicker: string, + amount: number, + orderType: 'market' | 'limit' = 'market', + triggerRatio?: number, + allowedSlippage: number = 0.5 +): Promise => { + try { + // Get markets and tokens data from GMX SDK with cache + const {marketsInfoData, tokensData} = await getMarketsInfoWithCache(sdk); + + if (!marketsInfoData || !tokensData) { + throw new Error("No markets or tokens info data"); + } + + // Get token data for from and to tokens + const fromTokenData = getTokenDataFromTicker(fromTicker, tokensData); + const toTokenData = getTokenDataFromTicker(toTicker, tokensData); + + console.log("fromTokenData", fromTokenData); + if (!fromTokenData || !toTokenData) { + throw new Error(`Token data not found for ${fromTicker} or ${toTicker}`); + } + + // Calculate the from token amount with proper decimals + const fromTokenAmount = BigInt(Math.floor(amount * Math.pow(10, fromTokenData.decimals))); + + // Check and handle token allowance for ExchangeRouter contract + const syntheticsRouterRouterAddress = getContract(sdk.chainId, "SyntheticsRouter"); + + try { + const currentAllowance = await getTokenAllowance( + sdk.account, + fromTokenData.address, + syntheticsRouterRouterAddress + ); + + console.log(`Current allowance for ${fromTicker}:`, currentAllowance.toString()); + console.log(`Required amount:`, fromTokenAmount.toString()); + + if (currentAllowance < fromTokenAmount) { + console.log(`🔧 Insufficient allowance for ${fromTicker}. Auto-approving ExchangeRouter...`); + + // Calculate a reasonable approval amount (use the larger of required amount or 1000 tokens) + const tokenUnit = BigInt(Math.pow(10, fromTokenData.decimals)); + const minApprovalAmount = tokenUnit * 1000n; // 1000 tokens + const approvalAmount = fromTokenAmount > minApprovalAmount ? fromTokenAmount : minApprovalAmount; + + const approvalHash = await approveContractImpl( + sdk.account, + fromTokenData.address, + syntheticsRouterRouterAddress, + sdk.chainId, + approvalAmount + ); + + console.log(`✅ Token approval successful! Hash: ${approvalHash}`); + console.log(`📝 Approved ${approvalAmount.toString()} ${fromTicker} for ExchangeRouter`); + } else { + console.log(`✅ Sufficient allowance already exists for ${fromTicker}`); + } + } catch (allowanceError) { + console.warn('Could not check or approve token allowance:', allowanceError); + throw new Error(`Failed to handle token allowance: ${allowanceError instanceof Error ? allowanceError.message : 'Unknown error'}`); + } + + // Calculate trigger price for limit orders + let triggerPrice: bigint | undefined; + if (orderType === 'limit' && triggerRatio) { + // Convert trigger ratio to price format (30 decimals) + triggerPrice = BigInt(Math.floor(triggerRatio * Math.pow(10, 30))); + } + + // Convert slippage percentage to basis points + const allowedSlippageBps = Math.floor(allowedSlippage * 100); // Convert percentage to basis points + + // Create SwapParams for the SDK + const swapParams = { + fromAmount: fromTokenAmount, + fromTokenAddress: fromTokenData.address, + toTokenAddress: toTokenData.address, + allowedSlippageBps: allowedSlippageBps, + referralCodeForTxn: encodeReferralCode("kaigen_ai"), + triggerPrice: triggerPrice, // Only defined for limit orders + skipSimulation: true, + } as SwapParams; + + console.log("swapParams", swapParams); + + swapParams.marketsInfoData = marketsInfoData; + swapParams.tokensData = tokensData; + + + // Use the SDK's built-in swap method + await sdk.orders.swap(swapParams); + + return "swap_order_created"; + } catch (error) { + console.error('Error swapping GMX tokens:', error); + throw new Error(`Failed to swap tokens: ${error instanceof Error ? error.message : 'Unknown error'}`); + } +}; + +/** + * Swaps tokens on GMX + * @param this The FastifyRequest instance + * @param reply The FastifyReply instance + * @param account The wallet address of the user + * @param fromTicker The ticker symbol of the token to swap from + * @param toTicker The ticker symbol of the token to swap to + * @param amount The amount to swap + * @param orderType The order type (market or limit) + * @param triggerRatio The trigger ratio for limit orders (optional) + * @param allowedSlippage The allowed slippage percentage (default 0.5%) + * @returns The response object with success status and transaction hash + */ +export async function swapGmxTokens( + this: FastifyRequest, + reply: FastifyReply, + account: string, + fromTicker: string, + toTicker: string, + amount: number, + orderType: 'market' | 'limit' = 'market', + triggerRatio?: number, + allowedSlippage: number = 0.5 +) { + try { + // Validate the request parameters + swapTokensSchema.parse({ + account, + fromTicker, + toTicker, + amount, + orderType, + triggerRatio, + allowedSlippage + }); + + // Get client for the address + const sdk = await this.getClientForAddress(account); + + // Call the implementation function + const hash = await swapGmxTokensImpl( + sdk, + fromTicker, + toTicker, + amount, + orderType, + triggerRatio, + allowedSlippage + ); + + return { + success: true, + hash, + message: `Successfully processed ${fromTicker} to ${toTicker} swap. Any required token approvals were handled automatically.` + }; + } catch (error) { + // Handle approval-specific errors with better messaging + if (error instanceof Error && error.message.includes('Failed to handle token allowance')) { + reply.status(400); + return { + success: false, + error: error.message, + errorType: 'APPROVAL_FAILED', + suggestion: `Token approval failed. Please ensure you have sufficient funds and try again, or manually approve the ExchangeRouter contract to spend your ${fromTicker} tokens.` + }; + } + + return handleError(this, reply, error, 'gmx/swap-tokens'); + } +} + +/** + * Checks token allowances for GMX contracts + * @param account The wallet address + * @param tokenAddress The token contract address + * @param chainId The chain ID (defaults to Arbitrum) + * @returns Object with allowances for different GMX contracts + */ +export const checkGmxTokenAllowances = async ( + account: string, + tokenAddress: string, + chainId: number = ARBITRUM +): Promise<{ + exchangeRouter: bigint; + orderVault: bigint; + syntheticsRouter: bigint; +}> => { + try { + const exchangeRouterAddress = getContract(chainId, "ExchangeRouter"); + const orderVaultAddress = getContract(chainId, "OrderVault"); + const syntheticsRouterAddress = getContract(chainId, "SyntheticsRouter"); + + const [exchangeRouterAllowance, orderVaultAllowance, syntheticsRouterAllowance] = await Promise.all([ + getTokenAllowance(account, tokenAddress, exchangeRouterAddress), + getTokenAllowance(account, tokenAddress, orderVaultAddress), + getTokenAllowance(account, tokenAddress, syntheticsRouterAddress) + ]); + + return { + exchangeRouter: exchangeRouterAllowance, + orderVault: orderVaultAllowance, + syntheticsRouter: syntheticsRouterAllowance + }; + } catch (error) { + console.error('Error checking GMX token allowances:', error); + throw new Error(`Failed to check token allowances: ${error instanceof Error ? error.message : 'Unknown error'}`); + } +}; diff --git a/src/Managing.Web3Proxy/test/plugins/close-position.test.ts b/src/Managing.Web3Proxy/test/plugins/close-position.test.ts index 4e5469e..3e75670 100644 --- a/src/Managing.Web3Proxy/test/plugins/close-position.test.ts +++ b/src/Managing.Web3Proxy/test/plugins/close-position.test.ts @@ -9,8 +9,8 @@ test('GMX Position Closing', async (t) => { const result = await closeGmxPositionImpl( sdk, - Ticker.AAVE, - TradeDirection.Short + Ticker.SOL, + TradeDirection.Long ) console.log('Position closing result:', result) assert.ok(result, 'Position closing result should be defined') diff --git a/src/Managing.Web3Proxy/test/plugins/swap-tokens.test.ts b/src/Managing.Web3Proxy/test/plugins/swap-tokens.test.ts new file mode 100644 index 0000000..d13f0bb --- /dev/null +++ b/src/Managing.Web3Proxy/test/plugins/swap-tokens.test.ts @@ -0,0 +1,32 @@ +import {before, describe, it} from 'node:test' +import assert from 'node:assert' +import {getClientForAddress, swapGmxTokensImpl} from '../../src/plugins/custom/gmx.js' +import {Ticker} from '../../src/generated/ManagingApiTypes' + +describe('swap tokens implementation', () => { + let sdk: any + + before(async () => { + // Initialize the SDK with a test account + const testAccount = '0xbba4eaa534cbd0ecaed5e2fd6036aec2e7ee309f' + sdk = await getClientForAddress(testAccount) + }) + + it('should swap USDC to ETH successfully', async () => { + try { + const result = await swapGmxTokensImpl( + sdk, + Ticker.GMX, // fromTicker + Ticker.USDC, // toTicker + 2.06 // amount + ) + + assert.strictEqual(typeof result, 'string') + assert.strictEqual(result, 'swap_order_created') + } catch (error) { + // Test that the error is related to actual execution rather than parameter validation + assert.ok(error instanceof Error) + console.log('Expected error during test execution:', error.message) + } + }) +})