Fix timeout error for getposition
This commit is contained in:
@@ -51,6 +51,11 @@ interface CacheEntry {
|
|||||||
const CACHE_TTL = 30 * 60 * 1000; // 30 minutes in milliseconds
|
const CACHE_TTL = 30 * 60 * 1000; // 30 minutes in milliseconds
|
||||||
const marketsCache = new Map<string, CacheEntry>();
|
const marketsCache = new Map<string, CacheEntry>();
|
||||||
|
|
||||||
|
// 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
|
* Gets markets info data from cache or fetches it from GMX SDK
|
||||||
* @param sdk The GMX SDK client
|
* @param sdk The GMX SDK client
|
||||||
@@ -134,7 +139,6 @@ const cancelOrdersSchema = z.object({
|
|||||||
ticker: z.string().nonempty()
|
ticker: z.string().nonempty()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Schema for claim funding fees request
|
// Schema for claim funding fees request
|
||||||
const claimFundingFeesSchema = z.object({
|
const claimFundingFeesSchema = z.object({
|
||||||
account: z.string().nonempty()
|
account: z.string().nonempty()
|
||||||
@@ -174,27 +178,25 @@ const swapTokensSchema = z.object({
|
|||||||
path: ["toTicker"]
|
path: ["toTicker"]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a GMX SDK client initialized for the given address
|
* Creates a GMX SDK client with the specified RPC URL
|
||||||
* If a walletId is provided, it will be used with Privy for signing
|
|
||||||
* @param account The wallet address to use
|
* @param account The wallet address to use
|
||||||
|
* @param rpcUrl The RPC URL to use for the client
|
||||||
* @returns An initialized GMX SDK client
|
* @returns An initialized GMX SDK client
|
||||||
*/
|
*/
|
||||||
export async function getClientForAddress(
|
export async function createGmxClientWithRpc(
|
||||||
account: string,
|
account: string,
|
||||||
|
rpcUrl: string = PRIMARY_RPC_URL
|
||||||
): Promise<GmxSdk> {
|
): Promise<GmxSdk> {
|
||||||
try {
|
try {
|
||||||
|
console.log(`Creating GMX client with RPC: ${rpcUrl}`);
|
||||||
|
|
||||||
console.log("GMX UI Fee Receiver:", process.env.GMX_UI_FEE_RECEIVER);
|
|
||||||
|
|
||||||
// Create SDK instance
|
// Create SDK instance
|
||||||
const arbitrumSdkConfig: GmxSdkConfig = {
|
const arbitrumSdkConfig: GmxSdkConfig = {
|
||||||
chainId: arbitrum.id,
|
chainId: arbitrum.id,
|
||||||
account: account,
|
account: account,
|
||||||
oracleUrl: "https://arbitrum-api.gmxinfra.io",
|
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",
|
subsquidUrl: "https://gmx.squids.live/gmx-synthetics-arbitrum:prod/api/graphql",
|
||||||
subgraphUrl: "https://subgraph.satsuma-prod.com/3b2ced13c8d9/gmx/synthetics-arbitrum-stats/api",
|
subgraphUrl: "https://subgraph.satsuma-prod.com/3b2ced13c8d9/gmx/synthetics-arbitrum-stats/api",
|
||||||
settings: {
|
settings: {
|
||||||
@@ -254,11 +256,23 @@ export async function getClientForAddress(
|
|||||||
|
|
||||||
return sdk;
|
return sdk;
|
||||||
} catch (error) {
|
} 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;
|
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<GmxSdk> {
|
||||||
|
return createGmxClientWithRpc(account, PRIMARY_RPC_URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementation function to open a position on GMX
|
* 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 sdk The GMX SDK client
|
||||||
|
* @param retryCount Current retry attempt count
|
||||||
* @returns Array of positions
|
* @returns Array of positions
|
||||||
*/
|
*/
|
||||||
export const getGmxPositionsImpl = async (
|
export const getGmxPositionsImpl = async (
|
||||||
sdk: GmxSdk
|
sdk: GmxSdk,
|
||||||
|
retryCount: number = 0
|
||||||
): Promise<Position[]> => {
|
): Promise<Position[]> => {
|
||||||
const {marketsInfoData, tokensData} = await sdk.markets.getMarketsInfo();
|
try {
|
||||||
|
const {marketsInfoData, tokensData} = await sdk.markets.getMarketsInfo();
|
||||||
|
|
||||||
const positionsInfo = await sdk.positions.getPositionsInfo({
|
const positionsInfo = await sdk.positions.getPositionsInfo({
|
||||||
marketsInfoData,
|
marketsInfoData,
|
||||||
tokensData,
|
tokensData,
|
||||||
showPnlInLeverage: true,
|
showPnlInLeverage: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const positions = Object.values(positionsInfo).map(async (pos) => {
|
const positions = Object.values(positionsInfo).map(async (pos) => {
|
||||||
// Fix leverage calculation to avoid exponential notation issues
|
// Fix leverage calculation to avoid exponential notation issues
|
||||||
let leverage = 1; // Default to 1x leverage
|
let leverage = 1; // Default to 1x leverage
|
||||||
|
|
||||||
if (pos.collateralAmount > 0n) {
|
if (pos.collateralAmount > 0n) {
|
||||||
// Manual calculation of leverage from raw values
|
// 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
|
// 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) {
|
if (pos.sizeInUsd > 0n && pos.collateralAmount > 0n) {
|
||||||
// Use a simplified approach to calculate leverage directly
|
// Use a simplified approach to calculate leverage directly
|
||||||
// Convert BigInt to Number before division to avoid mixing types
|
// Convert BigInt to Number before division to avoid mixing types
|
||||||
const sizeInUsdNumber = Number(pos.sizeInUsd);
|
const sizeInUsdNumber = Number(pos.sizeInUsd);
|
||||||
const collateralAmountNumber = Number(pos.collateralAmount);
|
const collateralAmountNumber = Number(pos.collateralAmount);
|
||||||
|
|
||||||
if (collateralAmountNumber > 0) {
|
if (collateralAmountNumber > 0) {
|
||||||
leverage = Math.round(sizeInUsdNumber / collateralAmountNumber);
|
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;
|
const ticker = pos.marketInfo.indexToken.symbol;
|
||||||
// get SL and TP from the position
|
// get SL and TP from the position
|
||||||
const trades = await getGmxTradeImpl(sdk, ticker);
|
const trades = await getGmxTradeImpl(sdk, ticker);
|
||||||
|
|
||||||
// Calculate proper price display decimals
|
// Calculate proper price display decimals
|
||||||
const priceDecimals = calculateDisplayDecimals(
|
const priceDecimals = calculateDisplayDecimals(
|
||||||
pos.indexToken.prices?.minPrice,
|
pos.indexToken.prices?.minPrice,
|
||||||
undefined,
|
undefined,
|
||||||
pos.indexToken.visualMultiplier
|
pos.indexToken.visualMultiplier
|
||||||
);
|
);
|
||||||
|
|
||||||
// Format prices using the same helper as trades for consistency
|
// Format prices using the same helper as trades for consistency
|
||||||
const displayEntryPrice = formatUsd(pos.entryPrice, {
|
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,
|
displayDecimals: priceDecimals,
|
||||||
visualMultiplier: pos.indexToken?.visualMultiplier,
|
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
|
// If it's not a multicall timeout or we've exceeded retry limit, re-throw the error
|
||||||
const numericEntryPrice = Number(displayEntryPrice.replace(/[^0-9.]/g, ''));
|
if (isMulticallTimeout && retryCount >= MAX_RETRY_ATTEMPTS) {
|
||||||
const numericMarkPrice = Number(displayMarkPrice.replace(/[^0-9.]/g, ''));
|
console.error(`❌ Maximum retry attempts (${MAX_RETRY_ATTEMPTS}) exceeded. Both primary and fallback RPCs failed.`);
|
||||||
let numericLiquidationPrice = undefined;
|
|
||||||
if (displayLiquidationPrice) {
|
|
||||||
numericLiquidationPrice = Number(displayLiquidationPrice.replace(/[^0-9.]/g, ''));
|
|
||||||
}
|
}
|
||||||
|
throw error;
|
||||||
// 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);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -954,7 +1005,7 @@ export async function getGmxPositions(
|
|||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const sdk = await this.getClientForAddress(account);
|
const sdk = await this.getClientForAddress(account);
|
||||||
const positions = await getGmxPositionsImpl(sdk);
|
const positions = await getGmxPositionsImpl(sdk, 0);
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
positions,
|
positions,
|
||||||
|
|||||||
Reference in New Issue
Block a user