Skip to content

update oracle keeper to use LB API endpoint with fallbacks #1815

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion sdk/src/configs/__tests__/markets.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type KeeperMarket = {
};

const getKeeperMarkets = async (chainId: number): Promise<{ markets: KeeperMarket[] }> => {
const res = await fetch(`${getOracleKeeperUrl(chainId, 0)}/markets`);
const res = await fetch(`${getOracleKeeperUrl(chainId)}/markets`);
const data = (await res.json()) as {
markets: KeeperMarket[];
};
Expand Down
2 changes: 1 addition & 1 deletion sdk/src/configs/__tests__/tokens.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type KeeperToken = {
};

const getKeeperTokens = async (chainId: number): Promise<{ tokens: KeeperToken[] }> => {
const res = await fetch(`${getOracleKeeperUrl(chainId, 0)}/tokens`);
const res = await fetch(`${getOracleKeeperUrl(chainId)}/tokens`);
const data = (await res.json()) as {
tokens: KeeperToken[];
};
Expand Down
55 changes: 19 additions & 36 deletions sdk/src/configs/oracleKeeper.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,37 @@
import random from "lodash/random";
import sample from "lodash/sample";

import { ARBITRUM, AVALANCHE, AVALANCHE_FUJI, BOTANIX, UiContractsChain } from "./chains";

const ORACLE_KEEPER_URLS: Record<UiContractsChain, string[]> = {
[ARBITRUM]: ["https://arbitrum-api.gmxinfra.io", "https://arbitrum-api.gmxinfra2.io"],
const ORACLE_KEEPER_URLS: Record<UiContractsChain, string> = {
[ARBITRUM]: "https://arbitrum-api.gmxinfra.io",

[AVALANCHE]: ["https://avalanche-api.gmxinfra.io", "https://avalanche-api.gmxinfra2.io"],
[AVALANCHE]: "https://avalanche-api.gmxinfra.io",

[AVALANCHE_FUJI]: ["https://synthetics-api-avax-fuji-upovm.ondigitalocean.app"],
[AVALANCHE_FUJI]: "https://synthetics-api-avax-fuji-upovm.ondigitalocean.app",

[BOTANIX]: ["https://botanix-api.gmxinfra.io", "https://botanix-api.gmxinfra2.io"],
[BOTANIX]: "https://botanix-api.gmxinfra.io",
};

export function getOracleKeeperUrl(chainId: number, index: number) {
const urls = ORACLE_KEEPER_URLS[chainId];
const ORACLE_KEEPER_FALLBACK_URLS: Record<UiContractsChain, string[]> = {
[ARBITRUM]: ["https://arbitrum-api-fallback.gmxinfra.io", "https://arbitrum-api-fallback.gmxinfra2.io"],

if (!urls.length) {
throw new Error(`No oracle keeper urls for chain ${chainId}`);
}
[AVALANCHE]: ["https://avalanche-api-fallback.gmxinfra.io", "https://avalanche-api-fallback.gmxinfra2.io"],

return urls[index] || urls[0];
}
[AVALANCHE_FUJI]: ["https://synthetics-api-avax-fuji-upovm.ondigitalocean.app"],

export function getOracleKeeperNextIndex(chainId: number, currentIndex: number) {
const urls = ORACLE_KEEPER_URLS[chainId];
[BOTANIX]: ["https://botanix-api-fallback.gmxinfra.io", "https://botanix-api-fallback.gmxinfra2.io"],
};

if (!urls.length) {
throw new Error(`No oracle keeper urls for chain ${chainId}`);
export function getOracleKeeperUrl(chainId: number) {
if (!ORACLE_KEEPER_URLS[chainId]) {
throw new Error(`No oracle keeper url for chain ${chainId}`);
}

return urls[currentIndex + 1] ? currentIndex + 1 : 0;
return ORACLE_KEEPER_URLS[chainId];
}

export function getOracleKeeperRandomIndex(chainId: number, bannedIndexes?: number[]): number {
const urls = ORACLE_KEEPER_URLS[chainId];

if (bannedIndexes?.length) {
const filteredUrls = urls.filter((url, i) => !bannedIndexes.includes(i));

if (filteredUrls.length) {
const url = sample(filteredUrls);

if (!url) {
throw new Error(`No oracle keeper urls for chain ${chainId}`);
}

return urls.indexOf(url);
}
export function getOracleKeeperFallbackUrls(chainId: number) {
if (!ORACLE_KEEPER_FALLBACK_URLS[chainId]) {
throw new Error(`No oracle keeper fallback urls for chain ${chainId}`);
}

return random(0, urls.length - 1);
return ORACLE_KEEPER_FALLBACK_URLS[chainId];
}
1 change: 0 additions & 1 deletion src/config/localStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ export const LEVERAGE_ENABLED_KEY = "leverage-enabled";
export const KEEP_LEVERAGE_FOR_DECREASE_KEY = "Exchange-keep-leverage";
export const TRADE_LINK_KEY = "trade-link";
export const SHOW_DEBUG_VALUES_KEY = "show-debug-values";
export const ORACLE_KEEPER_INSTANCES_CONFIG_KEY = "oracle-keeper-instances-config";
export const SORTED_MARKETS_KEY = "sorted-markets-key";
export const TWAP_NUMBER_OF_PARTS_KEY = "twap-number-of-parts";
export const TWAP_INFO_CARD_CLOSED_KEY = "twap-info-card-closed";
Expand Down
23 changes: 2 additions & 21 deletions src/context/SettingsContext/SettingsContextProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import noop from "lodash/noop";
import { Dispatch, ReactNode, SetStateAction, createContext, useContext, useEffect, useMemo, useState } from "react";
import { ReactNode, createContext, useContext, useEffect, useMemo, useState } from "react";

import { ARBITRUM, BOTANIX, EXECUTION_FEE_CONFIG_V2, SUPPORTED_CHAIN_IDS } from "config/chains";
import { ARBITRUM, BOTANIX, EXECUTION_FEE_CONFIG_V2 } from "config/chains";
import { isDevelopment } from "config/env";
import { DEFAULT_ACCEPTABLE_PRICE_IMPACT_BUFFER, DEFAULT_SLIPPAGE_AMOUNT } from "config/factors";
import {
Expand All @@ -10,7 +10,6 @@ import {
EXTERNAL_SWAPS_ENABLED_KEY,
IS_AUTO_CANCEL_TPSL_KEY,
IS_PNL_IN_LEVERAGE_KEY,
ORACLE_KEEPER_INSTANCES_CONFIG_KEY,
SETTINGS_WARNING_DOT_VISIBLE_KEY,
SHOULD_SHOW_POSITION_LINES_KEY,
SHOW_DEBUG_VALUES_KEY,
Expand All @@ -29,7 +28,6 @@ import { useLocalStorageByChainId, useLocalStorageSerializeKey } from "lib/local
import { tenderlyLsKeys } from "lib/tenderly";
import useWallet from "lib/wallets/useWallet";
import { getDefaultGasPaymentToken } from "sdk/configs/express";
import { getOracleKeeperRandomIndex } from "sdk/configs/oracleKeeper";
import { DEFAULT_TWAP_NUMBER_OF_PARTS } from "sdk/configs/twap";

export type SettingsContextType = {
Expand All @@ -42,8 +40,6 @@ export type SettingsContextType = {
setSavedAcceptablePriceImpactBuffer: (val: number) => void;
executionFeeBufferBps: number | undefined;
shouldUseExecutionFeeBuffer: boolean;
oracleKeeperInstancesConfig: { [chainId: number]: number };
setOracleKeeperInstancesConfig: Dispatch<SetStateAction<{ [chainId: number]: number } | undefined>>;
showPnlAfterFees: boolean;
setShowPnlAfterFees: (val: boolean) => void;
isPnlInLeverage: boolean;
Expand Down Expand Up @@ -128,17 +124,6 @@ export function SettingsContextProvider({ children }: { children: ReactNode }) {
);
const shouldUseExecutionFeeBuffer = Boolean(EXECUTION_FEE_CONFIG_V2[chainId].defaultBufferBps);

const [oracleKeeperInstancesConfig, setOracleKeeperInstancesConfig] = useLocalStorageSerializeKey(
ORACLE_KEEPER_INSTANCES_CONFIG_KEY,
SUPPORTED_CHAIN_IDS.reduce(
(acc, chainId) => {
acc[chainId] = getOracleKeeperRandomIndex(chainId);
return acc;
},
{} as { [chainId: number]: number }
)
);

const [savedShowPnlAfterFees, setSavedShowPnlAfterFees] = useLocalStorageSerializeKey(
[chainId, SHOW_PNL_AFTER_FEES_KEY],
true
Expand Down Expand Up @@ -237,8 +222,6 @@ export function SettingsContextProvider({ children }: { children: ReactNode }) {
executionFeeBufferBps,
setExecutionFeeBufferBps,
shouldUseExecutionFeeBuffer,
oracleKeeperInstancesConfig: oracleKeeperInstancesConfig!,
setOracleKeeperInstancesConfig,
savedAcceptablePriceImpactBuffer: savedAcceptablePriceImpactBuffer!,
setSavedAcceptablePriceImpactBuffer,
showPnlAfterFees: savedShowPnlAfterFees!,
Expand Down Expand Up @@ -292,8 +275,6 @@ export function SettingsContextProvider({ children }: { children: ReactNode }) {
executionFeeBufferBps,
setExecutionFeeBufferBps,
shouldUseExecutionFeeBuffer,
oracleKeeperInstancesConfig,
setOracleKeeperInstancesConfig,
savedAcceptablePriceImpactBuffer,
setSavedAcceptablePriceImpactBuffer,
savedShowPnlAfterFees,
Expand Down
4 changes: 0 additions & 4 deletions src/context/SyntheticsStateContext/hooks/settingsHooks.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import {
selectDebugSwapMarketsConfig,
selectExecutionFeeBufferBps,
selectOracleKeeperInstancesConfig,
selectSavedAcceptablePriceImpactBuffer,
selectSavedAllowedSlippage,
selectSetDebugSwapMarketsConfig,
selectSetExecutionFeeBufferBps,
selectSetOracleKeeperInstancesConfig,
selectSetSavedAcceptablePriceImpactBuffer,
selectSetSavedAllowedSlippage,
selectSetShowDebugValues,
Expand All @@ -24,7 +22,5 @@ export const useSetExecutionFeeBufferBps = () => useSelector(selectSetExecutionF
export const useSavedAcceptablePriceImpactBuffer = () => useSelector(selectSavedAcceptablePriceImpactBuffer);
export const useSetSavedAcceptablePriceImpactBuffer = () => useSelector(selectSetSavedAcceptablePriceImpactBuffer);
export const useShouldUseExecutionFeeBuffer = () => useSelector(selectShouldUseExecutionFeeBuffer);
export const useOracleKeeperInstancesConfig = () => useSelector(selectOracleKeeperInstancesConfig);
export const useSetOracleKeeperInstancesConfig = () => useSelector(selectSetOracleKeeperInstancesConfig);
export const useDebugSwapMarketsConfig = () => useSelector(selectDebugSwapMarketsConfig);
export const useSetDebugSwapMarketsConfig = () => useSelector(selectSetDebugSwapMarketsConfig);
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ export const selectSavedAcceptablePriceImpactBuffer = (s: SyntheticsState) =>
export const selectSetSavedAcceptablePriceImpactBuffer = (s: SyntheticsState) =>
s.settings.setSavedAcceptablePriceImpactBuffer;
export const selectShouldUseExecutionFeeBuffer = (s: SyntheticsState) => s.settings.shouldUseExecutionFeeBuffer;
export const selectOracleKeeperInstancesConfig = (s: SyntheticsState) => s.settings.oracleKeeperInstancesConfig;
export const selectSetOracleKeeperInstancesConfig = (s: SyntheticsState) => s.settings.setOracleKeeperInstancesConfig;
export const selectIsPnlInLeverage = (s: SyntheticsState) => s.settings.isPnlInLeverage;
export const selectShowPnlAfterFees = (s: SyntheticsState) => s.settings.showPnlAfterFees;
export const selectIsLeverageSliderEnabled = (s: SyntheticsState) => s.settings.isLeverageSliderEnabled;
Expand Down
2 changes: 1 addition & 1 deletion src/domain/synthetics/orders/simulation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
getMulticallContract,
getZeroAddressContract,
} from "config/contracts";
import { isDevelopment } from "config/env";
import { isGlvEnabled } from "domain/synthetics/markets/glv";
import { SwapPricingType } from "domain/synthetics/orders";
import { TokenPrices, TokensData, convertToContractPrice, getTokenData } from "domain/synthetics/tokens";
Expand All @@ -19,7 +20,6 @@ import { abis } from "sdk/abis";
import { convertTokenAddress } from "sdk/configs/tokens";
import { CustomErrorName, ErrorData, TxErrorType, extendError, isContractError, parseError } from "sdk/utils/errors";
import { CreateOrderTxnParams, ExternalCallsPayload } from "sdk/utils/orderTransactions";
import { isDevelopment } from "config/env";

export type SimulateExecuteParams = {
account: string;
Expand Down
84 changes: 41 additions & 43 deletions src/lib/oracleKeeperFetcher/oracleKeeperFetcher.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import random from "lodash/random";

import { isLocal } from "config/env";
import { Bar, FromNewToOldArray } from "domain/tradingview/types";
import { getOracleKeeperNextIndex, getOracleKeeperUrl } from "sdk/configs/oracleKeeper";
import { getOracleKeeperFallbackUrls, getOracleKeeperUrl } from "sdk/configs/oracleKeeper";
import { getNormalizedTokenSymbol } from "sdk/configs/tokens";
import { buildUrl } from "sdk/utils/buildUrl";

Expand All @@ -27,58 +29,54 @@ function parseOracleCandle(rawCandle: number[]): Bar {
};
}

let fallbackThrottleTimerId: any;
const failsPerMinuteToFallback = 5;

export class OracleKeeperFetcher implements OracleFetcher {
private readonly chainId: number;
private readonly oracleKeeperIndex: number;
private readonly setOracleKeeperInstancesConfig?: (
setter: (old: { [chainId: number]: number } | undefined) => {
[chainId: number]: number;
}
) => void;
public readonly url: string;
private readonly forceIncentivesActive: boolean;

constructor(p: {
chainId: number;
oracleKeeperIndex: number;
setOracleKeeperInstancesConfig?: (
setter: (old: { [chainId: number]: number } | undefined) => {
[chainId: number]: number;
}
) => void;
forceIncentivesActive: boolean;
}) {
private readonly forceIncentivesActive: boolean;
private isFallback: boolean;
private fallbackUrls: string[];
private fallbackThrottleTimerId: number | undefined;
private fallbackIndex: number;
private failTimes: number[];
private mainUrl: string;

constructor(p: { chainId: number; forceIncentivesActive: boolean }) {
this.chainId = p.chainId;
this.oracleKeeperIndex = p.oracleKeeperIndex;
this.setOracleKeeperInstancesConfig = p.setOracleKeeperInstancesConfig;
this.url = getOracleKeeperUrl(this.chainId, this.oracleKeeperIndex);
this.fallbackUrls = getOracleKeeperFallbackUrls(this.chainId);
this.mainUrl = getOracleKeeperUrl(this.chainId);
this.forceIncentivesActive = p.forceIncentivesActive;
this.isFallback = false;
this.failTimes = [];
}

get url() {
return this.isFallback ? this.fallbackUrls[this.fallbackIndex] : this.mainUrl;
}

switchOracleKeeper() {
if (fallbackThrottleTimerId || !this.setOracleKeeperInstancesConfig) {
handleFailure() {
if (this.fallbackThrottleTimerId) {
return;
}

const nextIndex = getOracleKeeperNextIndex(this.chainId, this.oracleKeeperIndex);
this.failTimes.push(Date.now());

if (nextIndex === this.oracleKeeperIndex) {
// eslint-disable-next-line no-console
console.error(`no available oracle keeper for chain ${this.chainId}`);
return;
}
this.failTimes = this.failTimes.filter((time) => time > Date.now() - 60000);

// eslint-disable-next-line no-console
console.log(`switch oracle keeper to ${getOracleKeeperUrl(this.chainId, nextIndex)}`);
if (this.failTimes.length >= failsPerMinuteToFallback) {
if (this.isFallback) {
this.fallbackIndex = (this.fallbackIndex + 1) % this.fallbackUrls.length;
} else {
this.fallbackIndex = random(0, this.fallbackUrls.length - 1);
}

this.setOracleKeeperInstancesConfig((old) => {
return { ...old, [this.chainId]: nextIndex };
});
this.isFallback = true;
this.failTimes = [];
}

fallbackThrottleTimerId = setTimeout(() => {
fallbackThrottleTimerId = undefined;
this.fallbackThrottleTimerId = window.setTimeout(() => {
this.fallbackThrottleTimerId = undefined;
}, 5000);
}

Expand All @@ -95,7 +93,7 @@ export class OracleKeeperFetcher implements OracleFetcher {
.catch((e) => {
// eslint-disable-next-line no-console
console.error(e);
this.switchOracleKeeper();
this.handleFailure();

throw e;
});
Expand All @@ -114,7 +112,7 @@ export class OracleKeeperFetcher implements OracleFetcher {
.catch((e) => {
// eslint-disable-next-line no-console
console.error(e);
this.switchOracleKeeper();
this.handleFailure();
throw e;
});
}
Expand Down Expand Up @@ -164,7 +162,7 @@ export class OracleKeeperFetcher implements OracleFetcher {
.catch((e) => {
// eslint-disable-next-line no-console
console.error(e);
this.switchOracleKeeper();
this.handleFailure();
throw e;
});
}
Expand All @@ -184,7 +182,7 @@ export class OracleKeeperFetcher implements OracleFetcher {
.catch((e) => {
// eslint-disable-next-line no-console
console.error(e);
this.switchOracleKeeper();
this.handleFailure();
throw e;
});
}
Expand All @@ -199,7 +197,7 @@ export class OracleKeeperFetcher implements OracleFetcher {
.catch((e) => {
// eslint-disable-next-line no-console
console.error(e);
this.switchOracleKeeper();
this.handleFailure();
return null;
});
}
Expand Down
21 changes: 10 additions & 11 deletions src/lib/oracleKeeperFetcher/useOracleKeeperFetcher.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
import { useMemo } from "react";

import { useSettings } from "context/SettingsContext/SettingsContextProvider";
import { useLocalStorageSerializeKey } from "lib/localStorage";
import { OracleFetcher, OracleKeeperFetcher } from "lib/oracleKeeperFetcher";

const oracleKeeperFetchersCached: Record<number, OracleFetcher> = {};

export function useOracleKeeperFetcher(chainId: number): OracleFetcher {
const { oracleKeeperInstancesConfig, setOracleKeeperInstancesConfig } = useSettings();
const oracleKeeperIndex = oracleKeeperInstancesConfig?.[chainId] ?? 0;
const [forceIncentivesActive] = useLocalStorageSerializeKey([chainId, "forceIncentivesActive"], false);

return useMemo(() => {
const instance = new OracleKeeperFetcher({
chainId,
oracleKeeperIndex,
forceIncentivesActive: Boolean(forceIncentivesActive),
setOracleKeeperInstancesConfig,
});
if (!oracleKeeperFetchersCached[chainId]) {
oracleKeeperFetchersCached[chainId] = new OracleKeeperFetcher({
chainId,
forceIncentivesActive: Boolean(forceIncentivesActive),
});
}

return instance;
}, [chainId, forceIncentivesActive, oracleKeeperIndex, setOracleKeeperInstancesConfig]);
return oracleKeeperFetchersCached[chainId];
}, [chainId, forceIncentivesActive]);
}
Loading