From 09a217ca63ee9e8f5ab13d1faa76a8d5dfbd1cd1 Mon Sep 17 00:00:00 2001 From: cryptooda Date: Sun, 17 Aug 2025 00:52:53 +0700 Subject: [PATCH] Fix timeout error for getposition --- .../src/plugins/custom/gmx.ts | 293 ++++++++++-------- 1 file changed, 172 insertions(+), 121 deletions(-) diff --git a/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts b/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts index 1208b67b..ca5dc7b0 100644 --- a/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts +++ b/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts @@ -51,6 +51,11 @@ interface CacheEntry { const CACHE_TTL = 30 * 60 * 1000; // 30 minutes in milliseconds const marketsCache = new Map(); +// Fallback RPC configuration +const FALLBACK_RPC_URL = "https://radial-shy-cherry.arbitrum-mainnet.quiknode.pro/098e57e961b05b24bcde008c4ca02fff6fb13b51/"; +const PRIMARY_RPC_URL = "https://arb1.arbitrum.io/rpc"; +const MAX_RETRY_ATTEMPTS = 2; // Only allow one retry to prevent infinite loops + /** * Gets markets info data from cache or fetches it from GMX SDK * @param sdk The GMX SDK client @@ -134,7 +139,6 @@ const cancelOrdersSchema = z.object({ ticker: z.string().nonempty() }); - // Schema for claim funding fees request const claimFundingFeesSchema = z.object({ account: z.string().nonempty() @@ -174,27 +178,25 @@ const swapTokensSchema = z.object({ 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 + * Creates a GMX SDK client with the specified RPC URL * @param account The wallet address to use + * @param rpcUrl The RPC URL to use for the client * @returns An initialized GMX SDK client */ -export async function getClientForAddress( +export async function createGmxClientWithRpc( account: string, + rpcUrl: string = PRIMARY_RPC_URL ): Promise { try { - - - console.log("GMX UI Fee Receiver:", process.env.GMX_UI_FEE_RECEIVER); + console.log(`Creating GMX client with RPC: ${rpcUrl}`); // Create SDK instance const arbitrumSdkConfig: GmxSdkConfig = { chainId: arbitrum.id, account: account, oracleUrl: "https://arbitrum-api.gmxinfra.io", - rpcUrl: "https://arb1.arbitrum.io/rpc", + rpcUrl: rpcUrl, subsquidUrl: "https://gmx.squids.live/gmx-synthetics-arbitrum:prod/api/graphql", subgraphUrl: "https://subgraph.satsuma-prod.com/3b2ced13c8d9/gmx/synthetics-arbitrum-stats/api", settings: { @@ -254,11 +256,23 @@ export async function getClientForAddress( return sdk; } catch (error) { - console.error(`Error getting GMX client: ${error instanceof Error ? error.message : 'Unknown error'}`); + console.error(`Error creating GMX client with RPC ${rpcUrl}: ${error instanceof Error ? error.message : 'Unknown error'}`); throw error; } } +/** + * Gets a GMX SDK client initialized for the given address + * If a walletId is provided, it will be used with Privy for signing + * @param account The wallet address to use + * @returns An initialized GMX SDK client + */ +export async function getClientForAddress( + account: string, +): Promise { + return createGmxClientWithRpc(account, PRIMARY_RPC_URL); +} + /** * Implementation function to open a position on GMX @@ -809,135 +823,172 @@ export async function getGmxTrade( } /** - * Implementation function to get positions on GMX + * Implementation function to get positions on GMX with fallback RPC support * @param sdk The GMX SDK client + * @param retryCount Current retry attempt count * @returns Array of positions */ export const getGmxPositionsImpl = async ( - sdk: GmxSdk + sdk: GmxSdk, + retryCount: number = 0 ): Promise => { - const {marketsInfoData, tokensData} = await sdk.markets.getMarketsInfo(); + try { + const {marketsInfoData, tokensData} = await sdk.markets.getMarketsInfo(); - const positionsInfo = await sdk.positions.getPositionsInfo({ - marketsInfoData, - tokensData, - showPnlInLeverage: true, - }); + const positionsInfo = await sdk.positions.getPositionsInfo({ + marketsInfoData, + tokensData, + showPnlInLeverage: true, + }); - const positions = Object.values(positionsInfo).map(async (pos) => { - // Fix leverage calculation to avoid exponential notation issues - let leverage = 1; // Default to 1x leverage + const positions = Object.values(positionsInfo).map(async (pos) => { + // Fix leverage calculation to avoid exponential notation issues + let leverage = 1; // Default to 1x leverage - if (pos.collateralAmount > 0n) { - // Manual calculation of leverage from raw values - // For positions, we already have the leverage value from GMX, but we want to ensure it's properly formatted - if (pos.sizeInUsd > 0n && pos.collateralAmount > 0n) { - // Use a simplified approach to calculate leverage directly - // Convert BigInt to Number before division to avoid mixing types - const sizeInUsdNumber = Number(pos.sizeInUsd); - const collateralAmountNumber = Number(pos.collateralAmount); - - if (collateralAmountNumber > 0) { - leverage = Math.round(sizeInUsdNumber / collateralAmountNumber); + if (pos.collateralAmount > 0n) { + // Manual calculation of leverage from raw values + // For positions, we already have the leverage value from GMX, but we want to ensure it's properly formatted + if (pos.sizeInUsd > 0n && pos.collateralAmount > 0n) { + // Use a simplified approach to calculate leverage directly + // Convert BigInt to Number before division to avoid mixing types + const sizeInUsdNumber = Number(pos.sizeInUsd); + const collateralAmountNumber = Number(pos.collateralAmount); + + if (collateralAmountNumber > 0) { + leverage = Math.round(sizeInUsdNumber / collateralAmountNumber); + } } } - } - const collateralDecimals = pos.isLong ? pos.marketInfo.shortToken.decimals : pos.marketInfo.longToken.decimals; + const collateralDecimals = pos.isLong ? pos.marketInfo.shortToken.decimals : pos.marketInfo.longToken.decimals; - const ticker = pos.marketInfo.indexToken.symbol; - // get SL and TP from the position - const trades = await getGmxTradeImpl(sdk, ticker); + const ticker = pos.marketInfo.indexToken.symbol; + // get SL and TP from the position + const trades = await getGmxTradeImpl(sdk, ticker); - // Calculate proper price display decimals - const priceDecimals = calculateDisplayDecimals( - pos.indexToken.prices?.minPrice, - undefined, - pos.indexToken.visualMultiplier - ); + // Calculate proper price display decimals + const priceDecimals = calculateDisplayDecimals( + pos.indexToken.prices?.minPrice, + undefined, + pos.indexToken.visualMultiplier + ); - // Format prices using the same helper as trades for consistency - const displayEntryPrice = formatUsd(pos.entryPrice, { - displayDecimals: priceDecimals, - visualMultiplier: pos.indexToken?.visualMultiplier, - }); - - const displayMarkPrice = formatUsd(pos.markPrice, { - displayDecimals: priceDecimals, - visualMultiplier: pos.indexToken?.visualMultiplier, - }); - - let displayLiquidationPrice = undefined; - if (pos.liquidationPrice) { - displayLiquidationPrice = formatUsd(pos.liquidationPrice, { + // Format prices using the same helper as trades for consistency + const displayEntryPrice = formatUsd(pos.entryPrice, { displayDecimals: priceDecimals, visualMultiplier: pos.indexToken?.visualMultiplier, }); + + const displayMarkPrice = formatUsd(pos.markPrice, { + displayDecimals: priceDecimals, + visualMultiplier: pos.indexToken?.visualMultiplier, + }); + + let displayLiquidationPrice = undefined; + if (pos.liquidationPrice) { + displayLiquidationPrice = formatUsd(pos.liquidationPrice, { + displayDecimals: priceDecimals, + visualMultiplier: pos.indexToken?.visualMultiplier, + }); + } + + // Remove currency symbols and convert to numbers + const numericEntryPrice = Number(displayEntryPrice.replace(/[^0-9.]/g, '')); + const numericMarkPrice = Number(displayMarkPrice.replace(/[^0-9.]/g, '')); + let numericLiquidationPrice = undefined; + if (displayLiquidationPrice) { + numericLiquidationPrice = Number(displayLiquidationPrice.replace(/[^0-9.]/g, '')); + } + + // Make sure we scale the token quantity properly + const tokenSize = bigintToNumber(pos.sizeInTokens, pos.marketInfo.indexToken.decimals); + + let stopLoss: Trade | undefined; + let takeProfit: Trade | undefined; + + if (pos.isLong) { + stopLoss = trades.find(t => t.price < numericEntryPrice); + takeProfit = trades.find(t => t.price > numericEntryPrice); + } else { + stopLoss = trades.find(t => t.price > numericEntryPrice); + takeProfit = trades.find(t => t.price < numericEntryPrice); + } + + let position = { + id: pos.key, + ticker: ticker, + direction: pos.isLong ? TradeDirection.Long : TradeDirection.Short, + price: numericEntryPrice, // Numeric entry price without currency symbol + quantity: tokenSize, // Position Size in tokens with correct decimals + leverage: leverage, // Fixed leverage value + status: PositionStatus.Filled, // Represents an open/active position + tradeType: TradeType.Market, // Placeholder + date: new Date(Number(pos.increasedAtTime) * 1000), // Position open time - convert BigInt to Number first + exchangeOrderId: pos.key, // Position key + pnl: bigintToNumber(pos.pnlAfterFees, PRECISION_DECIMALS), // PnL after fees (USD), assuming 30 decimals + collateral: bigintToNumber(pos.collateralAmount, collateralDecimals), // Collateral (in Collateral Token units) + markPrice: numericMarkPrice, // Numeric mark price without currency symbol + liquidationPrice: numericLiquidationPrice, // Numeric liquidation price without currency symbol + } as unknown as Position; // Use unknown assertion due to missing fields in target Position type + + if (stopLoss) { + position.stopLoss = stopLoss; + } + + if (takeProfit) { + position.takeProfit1 = takeProfit; + } + + // build the open trade base on the current position object + const open = { + direction: pos.isLong ? TradeDirection.Long : TradeDirection.Short, + price: numericEntryPrice, + quantity: tokenSize, + leverage: leverage, + status: TradeStatus.PendingOpen, + } as Trade; + + position.open = open; + + return position; + }); + + return Promise.all(positions); + } catch (error) { + // Check if this is a multicall timeout error and we haven't exceeded retry limit + const errorMessage = error instanceof Error ? error.message : String(error); + const isMulticallTimeout = errorMessage.toLowerCase().includes('multicall timeout') || + errorMessage.toLowerCase().includes('timeout') || + errorMessage.toLowerCase().includes('rpc error'); + + if (isMulticallTimeout && retryCount < MAX_RETRY_ATTEMPTS) { + console.log(`🔄 Multicall timeout detected (attempt ${retryCount + 1}/${MAX_RETRY_ATTEMPTS + 1}), retrying with fallback RPC...`); + + try { + // Create a new SDK client with the fallback RPC + const fallbackSdk = await createGmxClientWithRpc(sdk.account, FALLBACK_RPC_URL); + + console.log('✅ Fallback RPC client created, retrying positions request...'); + + // Recursively call the same function with the fallback SDK and incremented retry count + const result = await getGmxPositionsImpl(fallbackSdk, retryCount + 1); + + console.log('✅ Fallback RPC request successful'); + return result; + + } catch (fallbackError) { + console.error('❌ Fallback RPC also failed:', fallbackError); + // If fallback also fails, throw the original error + throw error; + } } - - // Remove currency symbols and convert to numbers - const numericEntryPrice = Number(displayEntryPrice.replace(/[^0-9.]/g, '')); - const numericMarkPrice = Number(displayMarkPrice.replace(/[^0-9.]/g, '')); - let numericLiquidationPrice = undefined; - if (displayLiquidationPrice) { - numericLiquidationPrice = Number(displayLiquidationPrice.replace(/[^0-9.]/g, '')); + + // If it's not a multicall timeout or we've exceeded retry limit, re-throw the error + if (isMulticallTimeout && retryCount >= MAX_RETRY_ATTEMPTS) { + console.error(`❌ Maximum retry attempts (${MAX_RETRY_ATTEMPTS}) exceeded. Both primary and fallback RPCs failed.`); } - - // Make sure we scale the token quantity properly - const tokenSize = bigintToNumber(pos.sizeInTokens, pos.marketInfo.indexToken.decimals); - - let stopLoss: Trade | undefined; - let takeProfit: Trade | undefined; - - if (pos.isLong) { - stopLoss = trades.find(t => t.price < numericEntryPrice); - takeProfit = trades.find(t => t.price > numericEntryPrice); - } else { - stopLoss = trades.find(t => t.price > numericEntryPrice); - takeProfit = trades.find(t => t.price < numericEntryPrice); - } - - let position = { - id: pos.key, - ticker: ticker, - direction: pos.isLong ? TradeDirection.Long : TradeDirection.Short, - price: numericEntryPrice, // Numeric entry price without currency symbol - quantity: tokenSize, // Position Size in tokens with correct decimals - leverage: leverage, // Fixed leverage value - status: PositionStatus.Filled, // Represents an open/active position - tradeType: TradeType.Market, // Placeholder - date: new Date(Number(pos.increasedAtTime) * 1000), // Position open time - convert BigInt to Number first - exchangeOrderId: pos.key, // Position key - pnl: bigintToNumber(pos.pnlAfterFees, PRECISION_DECIMALS), // PnL after fees (USD), assuming 30 decimals - collateral: bigintToNumber(pos.collateralAmount, collateralDecimals), // Collateral (in Collateral Token units) - markPrice: numericMarkPrice, // Numeric mark price without currency symbol - liquidationPrice: numericLiquidationPrice, // Numeric liquidation price without currency symbol - } as unknown as Position; // Use unknown assertion due to missing fields in target Position type - - if (stopLoss) { - position.stopLoss = stopLoss; - } - - if (takeProfit) { - position.takeProfit1 = takeProfit; - } - - // build the open trade base on the current position object - const open = { - direction: pos.isLong ? TradeDirection.Long : TradeDirection.Short, - price: numericEntryPrice, - quantity: tokenSize, - leverage: leverage, - status: TradeStatus.PendingOpen, - } as Trade; - - position.open = open; - - return position; - }); - - - return Promise.all(positions); + throw error; + } }; /** @@ -954,7 +1005,7 @@ export async function getGmxPositions( ) { try { const sdk = await this.getClientForAddress(account); - const positions = await getGmxPositionsImpl(sdk); + const positions = await getGmxPositionsImpl(sdk, 0); return { success: true, positions,