From 3eb37d52b4956af26357368694e9ee25a0b2777f Mon Sep 17 00:00:00 2001 From: micky Date: Fri, 20 Sep 2024 12:17:14 +0200 Subject: [PATCH 1/7] Revert "Revert "rollout static markets exp"" This reverts commit 2c479b5f9150086a2c8821f8d855cfd103a7ee3d. --- src/domain/synthetics/markets/useMarkets.ts | 95 ++----------------- .../markets/useMarketsInfoRequest/index.ts | 4 +- 2 files changed, 8 insertions(+), 91 deletions(-) diff --git a/src/domain/synthetics/markets/useMarkets.ts b/src/domain/synthetics/markets/useMarkets.ts index 8306d66ab5..992ae8aa09 100644 --- a/src/domain/synthetics/markets/useMarkets.ts +++ b/src/domain/synthetics/markets/useMarkets.ts @@ -1,18 +1,12 @@ import { useMemo } from "react"; import { ethers } from "ethers"; -import { getContract } from "config/contracts"; import { isMarketEnabled } from "config/markets"; import { convertTokenAddress, getToken } from "config/tokens"; -import { useMulticall } from "lib/multicall"; -import { CONFIG_UPDATE_INTERVAL } from "lib/timeConstants"; -import { getIsFlagEnabled } from "config/ab"; import { MarketsData } from "./types"; import { getMarketFullName } from "./utils"; -import SyntheticsReader from "abis/SyntheticsReader.json"; - import { MARKETS } from "config/markets"; export type MarketsResult = { @@ -21,20 +15,15 @@ export type MarketsResult = { error?: Error | undefined; }; -const MARKETS_COUNT = 100; - export function useMarkets(chainId: number): MarketsResult { - const staticMarketData = useMemo(() => { - const enabledMarkets = MARKETS[chainId]; - - if (!enabledMarkets) { - // eslint-disable-next-line no-console - console.warn(`Static markets data for chain ${chainId} not found`); + return useMemo(() => { + const markets = MARKETS[chainId]; - return null; + if (!markets) { + throw new Error(`Static markets data for chain ${chainId} not found`); } - return Object.values(enabledMarkets).reduce( + return Object.values(markets).reduce( (acc: MarketsResult, enabledMarketConfig) => { const market = enabledMarketConfig; @@ -67,79 +56,7 @@ export function useMarkets(chainId: number): MarketsResult { return acc; }, - { marketsData: {}, marketsAddresses: [], error: undefined } + { marketsData: {}, marketsAddresses: [] } ); }, [chainId]); - - const shouldUseStaticMarketKeys = staticMarketData && getIsFlagEnabled("testPrebuiltMarkets"); - - const freshData = useMarketsMulticall(chainId, { enabled: !shouldUseStaticMarketKeys }); - - return shouldUseStaticMarketKeys ? staticMarketData : freshData; -} - -function useMarketsMulticall(chainId: number, { enabled = true } = {}): MarketsResult { - const { data, error } = useMulticall(chainId, "useMarketsData", { - key: enabled ? [] : null, - - refreshInterval: CONFIG_UPDATE_INTERVAL, - - request: () => ({ - reader: { - contractAddress: getContract(chainId, "SyntheticsReader"), - abi: SyntheticsReader.abi, - calls: { - markets: { - methodName: "getMarkets", - params: [getContract(chainId, "DataStore"), 0, MARKETS_COUNT], - }, - }, - }, - }), - parseResponse: (res) => { - return res.data.reader.markets.returnValues.reduce( - (acc: { marketsData: MarketsData; marketsAddresses: string[] }, marketValues) => { - if (!isMarketEnabled(chainId, marketValues.marketToken)) { - return acc; - } - - try { - const indexToken = getToken(chainId, convertTokenAddress(chainId, marketValues.indexToken, "native")); - const longToken = getToken(chainId, marketValues.longToken); - const shortToken = getToken(chainId, marketValues.shortToken); - - const isSameCollaterals = marketValues.longToken === marketValues.shortToken; - const isSpotOnly = marketValues.indexToken === ethers.ZeroAddress; - - const name = getMarketFullName({ indexToken, longToken, shortToken, isSpotOnly }); - - acc.marketsData[marketValues.marketToken] = { - marketTokenAddress: marketValues.marketToken, - indexTokenAddress: marketValues.indexToken, - longTokenAddress: marketValues.longToken, - shortTokenAddress: marketValues.shortToken, - isSameCollaterals, - isSpotOnly, - name, - data: "", - }; - - acc.marketsAddresses.push(marketValues.marketToken); - } catch (e) { - // eslint-disable-next-line no-console - console.warn("unsupported market", e); - } - - return acc; - }, - { marketsData: {}, marketsAddresses: [] } - ); - }, - }); - - return { - marketsData: data?.marketsData, - marketsAddresses: data?.marketsAddresses, - error, - }; } diff --git a/src/domain/synthetics/markets/useMarketsInfoRequest/index.ts b/src/domain/synthetics/markets/useMarketsInfoRequest/index.ts index 049becf1e9..0373a4aed7 100644 --- a/src/domain/synthetics/markets/useMarketsInfoRequest/index.ts +++ b/src/domain/synthetics/markets/useMarketsInfoRequest/index.ts @@ -205,7 +205,7 @@ export type MarketConfigMulticallRequestConfig = MulticallRequestConfig<{ export function useMarketsInfoRequest(chainId: number): MarketsInfoResult { const { address: account } = useAccount(); - const { marketsData, marketsAddresses, error: marketsError } = useMarkets(chainId); + const { marketsData, marketsAddresses } = useMarkets(chainId); const { tokensData, pricesUpdatedAt, error: tokensDataError } = useTokensDataRequest(chainId); const isDependenciesLoading = !marketsAddresses || !tokensData; @@ -263,7 +263,7 @@ export function useMarketsInfoRequest(chainId: number): MarketsInfoResult { return data as MarketsInfoData; }, [marketsValues.data, marketsConfigs.data, marketsAddresses, marketsData, tokensData, chainId]); - const error = marketsError || tokensDataError || marketsValues.error || marketsConfigs.error; + const error = tokensDataError || marketsValues.error || marketsConfigs.error; return { marketsInfoData: isDependenciesLoading ? undefined : mergedData, From abea01ea5cf6425d20c5b4a8a3b2c0b5557429a9 Mon Sep 17 00:00:00 2001 From: micky Date: Fri, 20 Sep 2024 12:24:53 +0200 Subject: [PATCH 2/7] Revert "Revert "rollout rpc window fallback ab"" This reverts commit 5f86886f92c952ebc78eea0e3e09b00be37560aa. --- src/config/ab.ts | 3 +-- src/lib/multicall/Multicall.ts | 12 ++++-------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/config/ab.ts b/src/config/ab.ts index b4c3dae08c..cbcac80a61 100644 --- a/src/config/ab.ts +++ b/src/config/ab.ts @@ -1,7 +1,7 @@ import mapValues from "lodash/mapValues"; import { AB_FLAG_STORAGE_KEY } from "./localStorage"; -type Flag = "testRpcWindowFallback" | "testPrebuiltMarkets"; +type Flag = "testPrebuiltMarkets"; type AbFlag = { enabled: boolean; @@ -12,7 +12,6 @@ type AbStorage = { }; const abFlagsConfig: Record = { - testRpcWindowFallback: 0.5, testPrebuiltMarkets: 0.5, }; diff --git a/src/lib/multicall/Multicall.ts b/src/lib/multicall/Multicall.ts index 1dd84418d3..7d67565508 100644 --- a/src/lib/multicall/Multicall.ts +++ b/src/lib/multicall/Multicall.ts @@ -282,7 +282,7 @@ export class Multicall { // eslint-disable-next-line no-console console.groupEnd(); - if (!isFallbackMode && this.abFlags?.testRpcWindowFallback) { + if (!isFallbackMode) { this.fallbackRpcSwitcher?.trigger(); } @@ -373,14 +373,10 @@ export class Multicall { isAlchemy: isFallbackMode, }); - if (this.abFlags?.testRpcWindowFallback) { - if (!isFallbackMode) { - this.fallbackRpcSwitcher?.trigger(); - } - - return await fallbackMulticall(new Error("multicall fallback error")).then(processResponse); + if (!isFallbackMode) { + this.fallbackRpcSwitcher?.trigger(); } - return result; + return await fallbackMulticall(new Error("multicall fallback error")).then(processResponse); } } From f54d7e636920da743458e4a98e4e4615b0000f5e Mon Sep 17 00:00:00 2001 From: Divhead Date: Fri, 20 Sep 2024 13:51:33 +0300 Subject: [PATCH 3/7] fix value.hash error --- src/lib/contracts/utils.ts | 18 +++++++++++++++++- src/lib/metrics/Metrics.ts | 18 +++++++++++++++--- src/lib/metrics/emitMetricEvent.ts | 3 ++- src/lib/oracleKeeperFetcher.ts | 9 +++++++-- 4 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/lib/contracts/utils.ts b/src/lib/contracts/utils.ts index 881b8bc34c..ec4d1a3ed3 100644 --- a/src/lib/contracts/utils.ts +++ b/src/lib/contracts/utils.ts @@ -7,12 +7,28 @@ import { import { BASIS_POINTS_DIVISOR_BIGINT } from "config/factors"; import { BaseContract, Contract, Provider, Wallet } from "ethers"; import { bigMath } from "lib/bigmath"; +import { MetricEventParams } from "lib/metrics"; +import { emitMetricEvent } from "lib/metrics/emitMetricEvent"; +import { withRetry } from "viem"; export async function getGasPrice(provider: Provider, chainId: number) { let maxFeePerGas = MAX_FEE_PER_GAS_MAP[chainId]; const premium: bigint = GAS_PRICE_PREMIUM_MAP[chainId] || 0n; - const feeData = await provider.getFeeData(); + const feeData = await withRetry(() => provider.getFeeData(), { + delay: 200, + retryCount: 2, + shouldRetry: ({ error }) => { + const isInvalidBlockError = error?.message?.includes("invalid value for value.hash"); + + if (isInvalidBlockError) { + emitMetricEvent({ event: "error.getFeeData.value.hash", isCounter: true }); + } + + return isInvalidBlockError; + }, + }); + const gasPrice = feeData.gasPrice; if (maxFeePerGas) { diff --git a/src/lib/metrics/Metrics.ts b/src/lib/metrics/Metrics.ts index d4e76fda5b..421fd15e48 100644 --- a/src/lib/metrics/Metrics.ts +++ b/src/lib/metrics/Metrics.ts @@ -14,7 +14,8 @@ export type MetricEventParams = { event: string; data?: object; time?: number; - isError: boolean; + isError?: boolean; + isCounter?: boolean; }; const MAX_METRICS_STORE_TIME = 1000 * 60; // 1 min @@ -136,6 +137,10 @@ export class Metrics { throw new Error("Metrics: Fetcher is not initialized to send metric"); } + if (params.isCounter) { + return this.fetcher.fetchPostCounter({ event: params.event, abFlags: this.globalMetricData.abFlags }, this.debug); + } + const { time, isError, data, event } = params; const wallets = await getWalletNames(); @@ -147,7 +152,7 @@ export class Metrics { wallet: wallets.current, event: event, version: getAppVersion(), - isError, + isError: Boolean(isError), time, customFields: { ...(data ? this.serializeCustomFields(data) : {}), @@ -159,7 +164,14 @@ export class Metrics { ); }; - pushError = async (error: unknown, errorSource: string) => { + pushCounter(event: string) { + this.pushEvent({ + event, + isCounter: true, + }); + } + + pushError = (error: unknown, errorSource: string) => { const errorData = prepareErrorMetricData(error); if (!errorData) { diff --git a/src/lib/metrics/emitMetricEvent.ts b/src/lib/metrics/emitMetricEvent.ts index 0bd48bf028..a1933c2167 100644 --- a/src/lib/metrics/emitMetricEvent.ts +++ b/src/lib/metrics/emitMetricEvent.ts @@ -2,12 +2,13 @@ import { MetricEventParams } from "./Metrics"; export const METRIC_WINDOW_EVENT_NAME = "send-metric"; -export function emitMetricEvent({ event, data, time, isError }: T) { +export function emitMetricEvent({ event, data, time, isError, isCounter }: T) { globalThis.dispatchEvent( new CustomEvent(METRIC_WINDOW_EVENT_NAME, { detail: { event: event, isError: isError, + isCounter, data: data, time: time, }, diff --git a/src/lib/oracleKeeperFetcher.ts b/src/lib/oracleKeeperFetcher.ts index ad69c0c3a4..4aebddb71c 100644 --- a/src/lib/oracleKeeperFetcher.ts +++ b/src/lib/oracleKeeperFetcher.ts @@ -106,7 +106,7 @@ export interface OracleFetcher { fetchPostEvent(body: PostReport2Body, debug?: boolean): Promise; fetchPostFeedback(body: UserFeedbackBody, debug?: boolean): Promise; fetchPostTiming(body: { event: string; time: number; abFlags: Record }): Promise; - fetchPostCounter(body: { event: string; abFlags: Record }): Promise; + fetchPostCounter(body: { event: string; abFlags: Record }, debug?: boolean): Promise; } export class OracleKeeperFetcher implements OracleFetcher { @@ -248,7 +248,12 @@ export class OracleKeeperFetcher implements OracleFetcher { }); } - fetchPostCounter(body: { event: string; abFlags: Record }): Promise { + fetchPostCounter(body: { event: string; abFlags: Record }, debug?: boolean): Promise { + if (debug) { + // eslint-disable-next-line no-console + console.log("sendCounter", body); + } + return fetch(buildUrl(this.url!, "/report/ui/counter"), { method: "POST", headers: { From 7acaa3f35fdf05a72697516d547f176127a922a5 Mon Sep 17 00:00:00 2001 From: Divhead Date: Fri, 20 Sep 2024 14:07:04 +0300 Subject: [PATCH 4/7] fixes by review --- src/lib/contracts/utils.ts | 5 ++--- src/lib/metrics/Metrics.ts | 9 ++++++++- src/lib/metrics/emitMetricEvent.ts | 11 +++++++++++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/lib/contracts/utils.ts b/src/lib/contracts/utils.ts index ec4d1a3ed3..f796484df4 100644 --- a/src/lib/contracts/utils.ts +++ b/src/lib/contracts/utils.ts @@ -7,8 +7,7 @@ import { import { BASIS_POINTS_DIVISOR_BIGINT } from "config/factors"; import { BaseContract, Contract, Provider, Wallet } from "ethers"; import { bigMath } from "lib/bigmath"; -import { MetricEventParams } from "lib/metrics"; -import { emitMetricEvent } from "lib/metrics/emitMetricEvent"; +import { emitMetricCounter } from "lib/metrics/emitMetricEvent"; import { withRetry } from "viem"; export async function getGasPrice(provider: Provider, chainId: number) { @@ -22,7 +21,7 @@ export async function getGasPrice(provider: Provider, chainId: number) { const isInvalidBlockError = error?.message?.includes("invalid value for value.hash"); if (isInvalidBlockError) { - emitMetricEvent({ event: "error.getFeeData.value.hash", isCounter: true }); + emitMetricCounter({ event: "error.getFeeData.value.hash" }); } return isInvalidBlockError; diff --git a/src/lib/metrics/Metrics.ts b/src/lib/metrics/Metrics.ts index 421fd15e48..c3d0f0e15e 100644 --- a/src/lib/metrics/Metrics.ts +++ b/src/lib/metrics/Metrics.ts @@ -5,7 +5,7 @@ import { deserializeBigIntsInObject, serializeBigIntsInObject } from "lib/number import { sleep } from "lib/sleep"; import { getAppVersion } from "lib/version"; import { getWalletNames } from "lib/wallets/getWalletNames"; -import { METRIC_WINDOW_EVENT_NAME } from "./emitMetricEvent"; +import { METRIC_WINDOW_COUNTER_EVENT_NAME, METRIC_WINDOW_EVENT_NAME } from "./emitMetricEvent"; import { prepareErrorMetricData } from "./errorReporting"; import { getStorageItem, setStorageItem } from "./storage"; import { ErrorEvent, GlobalMetricData } from "./types"; @@ -197,12 +197,14 @@ export class Metrics { subscribeToEvents = () => { window.addEventListener(METRIC_WINDOW_EVENT_NAME, this.handleWindowEvent); + window.addEventListener(METRIC_WINDOW_COUNTER_EVENT_NAME, this.handleWindowCounter); window.addEventListener("error", this.handleError); window.addEventListener("unhandledrejection", this.handleUnhandledRejection); }; unsubscribeFromEvents = () => { window.removeEventListener(METRIC_WINDOW_EVENT_NAME, this.handleWindowEvent); + window.removeEventListener(METRIC_WINDOW_COUNTER_EVENT_NAME, this.handleWindowCounter); window.removeEventListener("error", this.handleError); window.removeEventListener("unhandledrejection", this.handleUnhandledRejection); }; @@ -212,6 +214,11 @@ export class Metrics { this.pushEvent(detail); }; + handleWindowCounter = (event: Event) => { + const { detail } = event as CustomEvent; + this.pushCounter(detail.event); + }; + handleError = (event) => { const error = event.error; diff --git a/src/lib/metrics/emitMetricEvent.ts b/src/lib/metrics/emitMetricEvent.ts index a1933c2167..a62aa203b3 100644 --- a/src/lib/metrics/emitMetricEvent.ts +++ b/src/lib/metrics/emitMetricEvent.ts @@ -1,6 +1,7 @@ import { MetricEventParams } from "./Metrics"; export const METRIC_WINDOW_EVENT_NAME = "send-metric"; +export const METRIC_WINDOW_COUNTER_EVENT_NAME = "send-counter"; export function emitMetricEvent({ event, data, time, isError, isCounter }: T) { globalThis.dispatchEvent( @@ -15,3 +16,13 @@ export function emitMetricEvent({ event, da }) ); } + +export function emitMetricCounter({ event }: { event: string }) { + globalThis.dispatchEvent( + new CustomEvent(METRIC_WINDOW_COUNTER_EVENT_NAME, { + detail: { + event: event, + }, + }) + ); +} From 356674caccff9f86df4ff6217cf56cbc04047cc4 Mon Sep 17 00:00:00 2001 From: Divhead Date: Fri, 20 Sep 2024 14:14:51 +0300 Subject: [PATCH 5/7] fixes by review --- src/lib/metrics/emitMetricEvent.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib/metrics/emitMetricEvent.ts b/src/lib/metrics/emitMetricEvent.ts index a62aa203b3..19c034f836 100644 --- a/src/lib/metrics/emitMetricEvent.ts +++ b/src/lib/metrics/emitMetricEvent.ts @@ -3,13 +3,12 @@ import { MetricEventParams } from "./Metrics"; export const METRIC_WINDOW_EVENT_NAME = "send-metric"; export const METRIC_WINDOW_COUNTER_EVENT_NAME = "send-counter"; -export function emitMetricEvent({ event, data, time, isError, isCounter }: T) { +export function emitMetricEvent({ event, data, time, isError }: T) { globalThis.dispatchEvent( new CustomEvent(METRIC_WINDOW_EVENT_NAME, { detail: { event: event, isError: isError, - isCounter, data: data, time: time, }, From 9bf84bcf30cdde36b6e610d69727ea991f4a9802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hub=C3=A9rt=20de=20Lalye?= Date: Fri, 20 Sep 2024 15:39:00 +0400 Subject: [PATCH 6/7] fixed GLV token allowance approvals --- .../GmDepositWithdrawalBox/GmDepositWithdrawalBox.tsx | 6 +++--- .../GmSwapBox/GmDepositWithdrawalBox/useTokensToApprove.tsx | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/Synthetics/GmSwap/GmSwapBox/GmDepositWithdrawalBox/GmDepositWithdrawalBox.tsx b/src/components/Synthetics/GmSwap/GmSwapBox/GmDepositWithdrawalBox/GmDepositWithdrawalBox.tsx index cac74ea837..d8c6804b36 100644 --- a/src/components/Synthetics/GmSwap/GmSwapBox/GmDepositWithdrawalBox/GmDepositWithdrawalBox.tsx +++ b/src/components/Synthetics/GmSwap/GmSwapBox/GmDepositWithdrawalBox/GmDepositWithdrawalBox.tsx @@ -909,7 +909,7 @@ export function GmSwapBoxDepositWithdrawal(p: GmSwapBoxProps) {
{submitState.tokensToApprove.map((address) => { const token = getTokenData(allTokensData, address)!; - const market = getByKey(marketsInfoData, address); + const marketOrGlv = getByKey(glvAndMarketsInfoData, address); let marketTokenData = address === marketToken?.address && getByKey(marketsInfoData, marketToken?.address); return (
@@ -918,8 +918,8 @@ export function GmSwapBoxDepositWithdrawal(p: GmSwapBoxProps) { tokenAddress={address} tokenSymbol={ marketTokenData - ? isGlvInfo(market) - ? market.glvToken.contractSymbol + ? isGlvInfo(marketOrGlv) + ? marketOrGlv.glvToken.contractSymbol : token.assetSymbol ?? token.symbol : token.assetSymbol ?? token.symbol } diff --git a/src/components/Synthetics/GmSwap/GmSwapBox/GmDepositWithdrawalBox/useTokensToApprove.tsx b/src/components/Synthetics/GmSwap/GmSwapBox/GmDepositWithdrawalBox/useTokensToApprove.tsx index 6212981147..9fcd74d215 100644 --- a/src/components/Synthetics/GmSwap/GmSwapBox/GmDepositWithdrawalBox/useTokensToApprove.tsx +++ b/src/components/Synthetics/GmSwap/GmSwapBox/GmDepositWithdrawalBox/useTokensToApprove.tsx @@ -63,7 +63,7 @@ export const useTokensToApprove = ({ } } } else if (operation === Operation.Withdrawal) { - addresses.push(marketToken.address); + addresses.push(glvToken ? glvToken.address : marketToken.address); } return uniq(addresses); @@ -76,6 +76,7 @@ export const useTokensToApprove = ({ shortTokenAmount, shortToken, glvInfo, + glvToken, isMarketTokenDeposit, marketTokenAmount, ] @@ -132,7 +133,7 @@ export const useTokensToApprove = ({ } } else if (operation === Operation.Withdrawal) { if (glvInfo && shouldApproveGlvToken) { - addresses.push(marketToken.address); + addresses.push(glvToken.address); } else if (!glvInfo && shouldApproveMarketToken) { addresses.push(marketToken.address); } From 1b4635b3c6ad88dabd2f627f673f4ef98f0901d0 Mon Sep 17 00:00:00 2001 From: micky Date: Fri, 13 Sep 2024 16:42:31 +0200 Subject: [PATCH 7/7] smart rpc switching --- src/config/ab.ts | 3 +- src/config/chains.ts | 14 +- src/config/localStorage.ts | 6 + .../SubaccountContext/SubaccountContext.tsx | 14 +- src/lib/localStorage/index.ts | 5 + src/lib/metrics/types.ts | 8 +- src/lib/multicall/Multicall.ts | 154 +++---- src/lib/multicall/executeMulticall.ts | 6 +- .../multicall/executeMulticallMainThread.ts | 8 +- src/lib/multicall/executeMulticallWorker.ts | 10 +- src/lib/multicall/multicall.worker.ts | 9 +- src/lib/oracleKeeperFetcher.ts | 18 +- src/lib/rpc/bestRpcTracker.ts | 397 ++++++++++++++++++ src/lib/rpc/getProviderNameFromUrl.ts | 18 + src/lib/rpc/index.ts | 18 +- 15 files changed, 573 insertions(+), 115 deletions(-) create mode 100644 src/lib/rpc/bestRpcTracker.ts create mode 100644 src/lib/rpc/getProviderNameFromUrl.ts diff --git a/src/config/ab.ts b/src/config/ab.ts index cbcac80a61..823b0bda1b 100644 --- a/src/config/ab.ts +++ b/src/config/ab.ts @@ -1,7 +1,7 @@ import mapValues from "lodash/mapValues"; import { AB_FLAG_STORAGE_KEY } from "./localStorage"; -type Flag = "testPrebuiltMarkets"; +type Flag = "testPrebuiltMarkets" | "testSmartRpcSwitching"; type AbFlag = { enabled: boolean; @@ -13,6 +13,7 @@ type AbStorage = { const abFlagsConfig: Record = { testPrebuiltMarkets: 0.5, + testSmartRpcSwitching: 0.5, }; const flags: Flag[] = Object.keys(abFlagsConfig) as Flag[]; diff --git a/src/config/chains.ts b/src/config/chains.ts index 5782b00eba..c3d3df9aa9 100644 --- a/src/config/chains.ts +++ b/src/config/chains.ts @@ -225,13 +225,17 @@ export const RPC_PROVIDERS = { "https://bsc-dataseed4.binance.org", ], [BSС_TESTNET]: ["https://data-seed-prebsc-1-s1.binance.org:8545/"], - [ARBITRUM]: ["https://arb1.arbitrum.io/rpc"], + [ARBITRUM]: ["https://arb1.arbitrum.io/rpc", "https://arbitrum-one-rpc.publicnode.com", "https://1rpc.io/arb"], [ARBITRUM_GOERLI]: [ "https://goerli-rollup.arbitrum.io/rpc", // "https://endpoints.omniatech.io/v1/arbitrum/goerli/public", // "https://arbitrum-goerli.public.blastapi.io", ], - [AVALANCHE]: ["https://api.avax.network/ext/bc/C/rpc"], + [AVALANCHE]: [ + "https://api.avax.network/ext/bc/C/rpc", + "https://avalanche-c-chain-rpc.publicnode.com", + "https://1rpc.io/avax/c", + ], [AVALANCHE_FUJI]: [ "https://avalanche-fuji-c-chain.publicnode.com", "https://api.avax-test.network/ext/bc/C/rpc", @@ -336,11 +340,7 @@ export function getChainName(chainId: number) { return CHAIN_NAMES_MAP[chainId]; } -export function getRpcUrl(chainId: number): string | undefined { - return sample(RPC_PROVIDERS[chainId]); -} - -export function getFallbackRpcUrl(chainId: number): string | undefined { +export function getFallbackRpcUrl(chainId: number): string { return sample(FALLBACK_PROVIDERS[chainId]); } diff --git a/src/config/localStorage.ts b/src/config/localStorage.ts index 4c9146ded7..f9390693ae 100644 --- a/src/config/localStorage.ts +++ b/src/config/localStorage.ts @@ -59,6 +59,8 @@ export const DEBUG_MULTICALL_BATCHING_KEY = "debug-multicall-batching"; export const AB_FLAG_STORAGE_KEY = "ab-flags"; +export const RPC_PROVIDER = "rpc-provider"; + export const getSubgraphUrlKey = (chainId: number, subgraph: string) => `subgraphUrl:${chainId}:${subgraph}`; export function getSyntheticsDepositIndexTokenKey(chainId: number) { @@ -109,6 +111,10 @@ export function getExecutionFeeBufferBpsKey(chainId: number) { return [chainId, EXECUTION_FEE_BUFFER_BPS_KEY]; } +export function getRpcProviderKey(chainId: number | string) { + return [chainId, RPC_PROVIDER]; +} + // TODO: this was made on 07.06.2024, remove this in 6 months, because everyone would be migrated to new defaults by then export function getHasOverriddenDefaultArb30ExecutionFeeBufferBpsKey(chainId: number) { return [chainId, HAS_OVERRIDDEN_DEFAULT_ARB_30_EXECUTION_FEE_BUFFER_BPS_KEY]; diff --git a/src/context/SubaccountContext/SubaccountContext.tsx b/src/context/SubaccountContext/SubaccountContext.tsx index 664ab66827..f8257205ef 100644 --- a/src/context/SubaccountContext/SubaccountContext.tsx +++ b/src/context/SubaccountContext/SubaccountContext.tsx @@ -4,9 +4,8 @@ import { ARBITRUM, AVALANCHE, AVALANCHE_FUJI, - NETWORK_EXECUTION_TO_CREATE_FEE_FACTOR, - getRpcUrl, getFallbackRpcUrl, + NETWORK_EXECUTION_TO_CREATE_FEE_FACTOR, } from "config/chains"; import { getContract } from "config/contracts"; import { @@ -40,6 +39,7 @@ import { Context, PropsWithChildren, useCallback, useEffect, useMemo, useState } import { createContext, useContextSelector } from "use-context-selector"; import { clientToSigner } from "lib/wallets/useEthersSigner"; import { estimateOrderOraclePriceCount } from "domain/synthetics/fees/utils/estimateOraclePriceCount"; +import { useBestRpcUrl } from "lib/rpc/bestRpcTracker"; export type Subaccount = ReturnType; @@ -302,13 +302,13 @@ function useSubaccountCustomSigners() { const { chainId } = useChainId(); const privateKey = useSubaccountPrivateKey(); - return useMemo(() => { - const publicRpc = getRpcUrl(chainId); - const fallbackRpc = getFallbackRpcUrl(chainId); + const primaryRpc = useBestRpcUrl(chainId); + const fallbackRpc = getFallbackRpcUrl(chainId); + return useMemo(() => { const rpcUrls: string[] = []; - if (publicRpc) rpcUrls.push(publicRpc); + if (primaryRpc) rpcUrls.push(primaryRpc); if (fallbackRpc) rpcUrls.push(fallbackRpc); if (!rpcUrls.length || !privateKey) return undefined; @@ -320,7 +320,7 @@ function useSubaccountCustomSigners() { return new ethers.Wallet(privateKey, provider); }); - }, [chainId, privateKey]); + }, [chainId, privateKey, primaryRpc, fallbackRpc]); } export function useSubaccount(requiredBalance: bigint | null, requiredActions = 1) { diff --git a/src/lib/localStorage/index.ts b/src/lib/localStorage/index.ts index 8e5719aade..df563f545a 100644 --- a/src/lib/localStorage/index.ts +++ b/src/lib/localStorage/index.ts @@ -1,5 +1,6 @@ import { useCallback } from "react"; import { useLocalStorage } from "react-use"; +import { SHOW_DEBUG_VALUES_KEY } from "config/localStorage"; export function useLocalStorageByChainId( chainId: number, @@ -51,3 +52,7 @@ export function useLocalStorageSerializeKey( return useLocalStorage(key, initialValue, opts); } + +export function isDebugMode() { + return localStorage.getItem(JSON.stringify(SHOW_DEBUG_VALUES_KEY)) === "true"; +} diff --git a/src/lib/metrics/types.ts b/src/lib/metrics/types.ts index 50a88667a3..376a11c5b2 100644 --- a/src/lib/metrics/types.ts +++ b/src/lib/metrics/types.ts @@ -146,8 +146,8 @@ export type MulticallTimeoutEvent = { data: { metricType: "rpcTimeout" | "multicallTimeout" | "workerTimeout"; isInMainThread: boolean; - isFallback?: boolean; - isAlchemy?: boolean; + requestType?: "initial" | "retry"; + rpcProvider?: string; errorMessage: string; }; }; @@ -157,8 +157,8 @@ export type MulticallErrorEvent = { isError: true; data: { isInMainThread: boolean; - isFallback?: boolean; - isAlchemy?: boolean; + rpcProvider?: string; + requestType?: "initial" | "retry"; errorMessage: string; }; }; diff --git a/src/lib/multicall/Multicall.ts b/src/lib/multicall/Multicall.ts index 7d67565508..24290b7852 100644 --- a/src/lib/multicall/Multicall.ts +++ b/src/lib/multicall/Multicall.ts @@ -2,7 +2,7 @@ import { ClientConfig, createPublicClient, http } from "viem"; import type { BatchOptions } from "viem/_types/clients/transports/http"; import { arbitrum, arbitrumGoerli, avalanche, avalancheFuji } from "viem/chains"; -import { ARBITRUM, ARBITRUM_GOERLI, AVALANCHE, AVALANCHE_FUJI, getFallbackRpcUrl, getRpcUrl } from "config/chains"; +import { ARBITRUM, ARBITRUM_GOERLI, AVALANCHE, AVALANCHE_FUJI } from "config/chains"; import { isWebWorker } from "config/env"; import { hashData } from "lib/hash"; import { sleep } from "lib/sleep"; @@ -14,6 +14,7 @@ import { emitMetricEvent } from "lib/metrics/emitMetricEvent"; import { SlidingWindowFallbackSwitcher } from "lib/slidingWindowFallbackSwitcher"; import { getStaticOracleKeeperFetcher } from "lib/oracleKeeperFetcher"; import { serializeMulticallErrors } from "./utils"; +import { getProviderNameFromUrl } from "lib/rpc/getProviderNameFromUrl"; export const MAX_TIMEOUT = 20000; @@ -24,6 +25,11 @@ const CHAIN_BY_CHAIN_ID = { [AVALANCHE]: avalanche, }; +export type MulticallProviderUrls = { + primary: string; + secondary: string; +}; + const BATCH_CONFIGS: Record< number, { @@ -90,14 +96,7 @@ export class Multicall { let instance = Multicall.instances[chainId]; if (!instance || instance.chainId !== chainId) { - const rpcUrl = getRpcUrl(chainId); - const fallbackRpcUrl = getFallbackRpcUrl(chainId); - - if (!rpcUrl || !fallbackRpcUrl) { - return undefined; - } - - instance = new Multicall(chainId, rpcUrl, fallbackRpcUrl, abFlags); + instance = new Multicall(chainId, abFlags); Multicall.instances[chainId] = instance; } @@ -126,14 +125,20 @@ export class Multicall { eventsThreshold: 3, onFallback: () => { getStaticOracleKeeperFetcher(this.chainId).fetchPostCounter({ - event: `multicall.${isWebWorker ? "worker" : "mainThread"}.fallbackRpcMode.on`, + event: `multicall.fallbackRpcMode.on`, abFlags: this.abFlags, + customFields: { + isInMainThread: !isWebWorker, + }, }); }, onRestore: () => { getStaticOracleKeeperFetcher(this.chainId).fetchPostCounter({ - event: `multicall.${isWebWorker ? "worker" : "mainThread"}.fallbackRpcMode.off`, + event: `multicall.fallbackRpcMode.off`, abFlags: this.abFlags, + customFields: { + isInMainThread: !isWebWorker, + }, }); }, }); @@ -142,23 +147,10 @@ export class Multicall { constructor( public chainId: number, - public rpcUrl: string, - public fallbackRpcUrl: string, private abFlags: Record - ) { - const client = Multicall.getViemClient(chainId, rpcUrl); - const fallbackClient = Multicall.getViemClient(chainId, fallbackRpcUrl); + ) {} - this.getClient = function getViemClient({ forceFallback = false } = {}) { - if (forceFallback || this.fallbackRpcSwitcher?.isFallbackMode) { - return fallbackClient; - } - - return client; - }; - } - - async call(request: MulticallRequestConfig, maxTimeout: number) { + async call(providerUrls: MulticallProviderUrls, request: MulticallRequestConfig, maxTimeout: number) { const originalKeys: { contractKey: string; callKey: string; @@ -208,18 +200,23 @@ export class Multicall { }); }); - const client = this.getClient(); - const isFallbackMode = this.fallbackRpcSwitcher?.isFallbackMode; + const providerUrl = this.fallbackRpcSwitcher?.isFallbackMode ? providerUrls.secondary : providerUrls.primary; + const client = Multicall.getViemClient(this.chainId, providerUrl); + const isAlchemy = providerUrl === providerUrls.secondary; + const rpcProviderName = getProviderNameFromUrl(providerUrl); const sendCounterEvent = ( - event: string, - { isFallback, isAlchemy }: { isFallback: boolean; isAlchemy: boolean } + event: "call" | "timeout" | "error", + { requestType, rpcProvider }: { requestType: "initial" | "retry"; rpcProvider: string } ) => { getStaticOracleKeeperFetcher(this.chainId).fetchPostCounter({ - event: ["multicall", isAlchemy ? "alchemy" : "public", isFallback ? "fallback" : "primary", event] - .filter(Boolean) - .join("."), + event: `multicall.request.${event}`, abFlags: this.abFlags, + customFields: { + isInMainThread: !isWebWorker, + requestType, + rpcProvider, + }, }); }; @@ -270,9 +267,12 @@ export class Multicall { }; const fallbackMulticall = (e: Error) => { - sendCounterEvent("request", { - isFallback: true, - isAlchemy: true, + const fallbackProviderUrl = providerUrls.secondary; + const fallbackProviderName = getProviderNameFromUrl(fallbackProviderUrl); + + sendCounterEvent("call", { + requestType: "retry", + rpcProvider: fallbackProviderName, }); // eslint-disable-next-line no-console @@ -282,47 +282,47 @@ export class Multicall { // eslint-disable-next-line no-console console.groupEnd(); - if (!isFallbackMode) { + if (!isAlchemy) { this.fallbackRpcSwitcher?.trigger(); } // eslint-disable-next-line no-console console.debug(`using multicall fallback for chain ${this.chainId}`); - return this.getClient({ forceFallback: true }) - .multicall({ contracts: encodedPayload as any }) - .catch((_viemError) => { - const e = new Error(_viemError.message.slice(0, 150)); - // eslint-disable-next-line no-console - console.groupCollapsed("multicall fallback error:"); - // eslint-disable-next-line no-console - console.error(e); - // eslint-disable-next-line no-console - console.groupEnd(); - - emitMetricEvent({ - event: "multicall.error", - isError: true, - data: { - isFallback: true, - isAlchemy: true, - isInMainThread: !isWebWorker, - errorMessage: _viemError.message, - }, - }); - - sendCounterEvent("error", { - isFallback: true, - isAlchemy: true, - }); - - throw e; + const fallbackClient = Multicall.getViemClient(this.chainId, fallbackProviderUrl); + + return fallbackClient.multicall({ contracts: encodedPayload as any }).catch((_viemError) => { + const e = new Error(_viemError.message.slice(0, 150)); + // eslint-disable-next-line no-console + console.groupCollapsed("multicall fallback error:"); + // eslint-disable-next-line no-console + console.error(e); + // eslint-disable-next-line no-console + console.groupEnd(); + + emitMetricEvent({ + event: "multicall.error", + isError: true, + data: { + requestType: "retry", + rpcProvider: fallbackProviderName, + isInMainThread: !isWebWorker, + errorMessage: _viemError.message, + }, + }); + + sendCounterEvent("error", { + requestType: "retry", + rpcProvider: fallbackProviderName, }); + + throw e; + }); }; - sendCounterEvent("request", { - isFallback: false, - isAlchemy: isFallbackMode, + sendCounterEvent("call", { + requestType: "initial", + rpcProvider: rpcProviderName, }); const result = await Promise.race([ @@ -339,15 +339,15 @@ export class Multicall { data: { metricType: "rpcTimeout", isInMainThread: !isWebWorker, - isFallback: false, - isAlchemy: isFallbackMode, + requestType: "initial", + rpcProvider: rpcProviderName, errorMessage: _viemError.message.slice(0, 150), }, }); sendCounterEvent("timeout", { - isFallback: false, - isAlchemy: isFallbackMode, + requestType: "initial", + rpcProvider: rpcProviderName, }); return fallbackMulticall(e).then(processResponse); @@ -361,19 +361,19 @@ export class Multicall { event: "multicall.error", isError: true, data: { - isFallback: false, - isAlchemy: isFallbackMode, + requestType: "initial", + rpcProvider: rpcProviderName, isInMainThread: !isWebWorker, errorMessage: serializeMulticallErrors(result.errors), }, }); sendCounterEvent("error", { - isFallback: false, - isAlchemy: isFallbackMode, + requestType: "initial", + rpcProvider: rpcProviderName, }); - if (!isFallbackMode) { + if (!isAlchemy) { this.fallbackRpcSwitcher?.trigger(); } diff --git a/src/lib/multicall/executeMulticall.ts b/src/lib/multicall/executeMulticall.ts index ad6955f7a4..a8ae65057c 100644 --- a/src/lib/multicall/executeMulticall.ts +++ b/src/lib/multicall/executeMulticall.ts @@ -272,16 +272,16 @@ export function executeMulticall>( throttledExecuteBackgroundChainsMulticalls(); } - const now = Date.now(); + const durationStart = performance.now(); return promise.then((result) => { - const duration = Date.now() - now; + const duration = performance.now() - durationStart; const abFlags = getAbFlags(); if (result.success) { getStaticOracleKeeperFetcher(chainId).fetchPostTiming({ event: `multicall.${priority}.execute.timing`, - time: duration, + time: Math.round(duration), abFlags, }); } else { diff --git a/src/lib/multicall/executeMulticallMainThread.ts b/src/lib/multicall/executeMulticallMainThread.ts index 275f3f5c54..56cc3b0db5 100644 --- a/src/lib/multicall/executeMulticallMainThread.ts +++ b/src/lib/multicall/executeMulticallMainThread.ts @@ -1,9 +1,15 @@ import { MAX_TIMEOUT, Multicall } from "./Multicall"; import type { MulticallRequestConfig } from "./types"; import { getAbFlags } from "config/ab"; +import { getBestRpcUrl } from "lib/rpc/bestRpcTracker"; +import { getFallbackRpcUrl } from "config/chains"; export async function executeMulticallMainThread(chainId: number, request: MulticallRequestConfig) { const multicall = await Multicall.getInstance(chainId, getAbFlags()); + const providerUrls = { + primary: getBestRpcUrl(chainId), + secondary: getFallbackRpcUrl(chainId), + }; - return multicall?.call(request, MAX_TIMEOUT); + return multicall?.call(providerUrls, request, MAX_TIMEOUT); } diff --git a/src/lib/multicall/executeMulticallWorker.ts b/src/lib/multicall/executeMulticallWorker.ts index 88245b23ad..455b2be3ef 100644 --- a/src/lib/multicall/executeMulticallWorker.ts +++ b/src/lib/multicall/executeMulticallWorker.ts @@ -5,11 +5,13 @@ import { sleep } from "lib/sleep"; import { promiseWithResolvers } from "lib/utils"; import { emitMetricEvent } from "lib/metrics/emitMetricEvent"; -import { MAX_TIMEOUT } from "./Multicall"; +import { MAX_TIMEOUT, MulticallProviderUrls } from "./Multicall"; import { executeMulticallMainThread } from "./executeMulticallMainThread"; import type { MulticallRequestConfig, MulticallResult } from "./types"; import { MetricEventParams, MulticallTimeoutEvent } from "lib/metrics"; import { getAbFlags } from "config/ab"; +import { getBestRpcUrl } from "lib/rpc/bestRpcTracker"; +import { getFallbackRpcUrl } from "config/chains"; const executorWorker: Worker = new Worker(new URL("./multicall.worker", import.meta.url), { type: "module" }); @@ -56,9 +58,15 @@ export async function executeMulticallWorker( ): Promise | undefined> { const id = uniqueId("multicall-"); + const providerUrls: MulticallProviderUrls = { + primary: getBestRpcUrl(chainId), + secondary: getFallbackRpcUrl(chainId), + }; + executorWorker.postMessage({ id, chainId, + providerUrls, request, abFlags: getAbFlags(), PRODUCTION_PREVIEW_KEY: localStorage.getItem(PRODUCTION_PREVIEW_KEY), diff --git a/src/lib/multicall/multicall.worker.ts b/src/lib/multicall/multicall.worker.ts index 8e484cbaf1..eb1b0306cd 100644 --- a/src/lib/multicall/multicall.worker.ts +++ b/src/lib/multicall/multicall.worker.ts @@ -1,27 +1,28 @@ import { METRIC_WINDOW_EVENT_NAME } from "lib/metrics/emitMetricEvent"; -import { MAX_TIMEOUT, Multicall } from "./Multicall"; +import { MAX_TIMEOUT, Multicall, MulticallProviderUrls } from "./Multicall"; import type { MulticallRequestConfig } from "./types"; async function executeMulticall( chainId: number, + providerUrls: MulticallProviderUrls, request: MulticallRequestConfig, abFlags: Record ) { const multicall = await Multicall.getInstance(chainId, abFlags); - return multicall?.call(request, MAX_TIMEOUT); + return multicall?.call(providerUrls, request, MAX_TIMEOUT); } self.addEventListener("message", run); async function run(event) { - const { PRODUCTION_PREVIEW_KEY, chainId, request, id, abFlags } = event.data; + const { PRODUCTION_PREVIEW_KEY, chainId, providerUrls, request, id, abFlags } = event.data; // @ts-ignore self.PRODUCTION_PREVIEW_KEY = PRODUCTION_PREVIEW_KEY; try { - const result = await executeMulticall(chainId, request, abFlags); + const result = await executeMulticall(chainId, providerUrls, request, abFlags); postMessage({ id, diff --git a/src/lib/oracleKeeperFetcher.ts b/src/lib/oracleKeeperFetcher.ts index 4aebddb71c..fa7cf70a58 100644 --- a/src/lib/oracleKeeperFetcher.ts +++ b/src/lib/oracleKeeperFetcher.ts @@ -106,7 +106,14 @@ export interface OracleFetcher { fetchPostEvent(body: PostReport2Body, debug?: boolean): Promise; fetchPostFeedback(body: UserFeedbackBody, debug?: boolean): Promise; fetchPostTiming(body: { event: string; time: number; abFlags: Record }): Promise; - fetchPostCounter(body: { event: string; abFlags: Record }, debug?: boolean): Promise; + fetchPostCounter( + body: { + event: string; + abFlags: Record; + customFields?: Record; + }, + debug?: boolean + ): Promise; } export class OracleKeeperFetcher implements OracleFetcher { @@ -248,7 +255,14 @@ export class OracleKeeperFetcher implements OracleFetcher { }); } - fetchPostCounter(body: { event: string; abFlags: Record }, debug?: boolean): Promise { + fetchPostCounter( + body: { + event: string; + abFlags: Record; + customFields?: Record; + }, + debug?: boolean + ): Promise { if (debug) { // eslint-disable-next-line no-console console.log("sendCounter", body); diff --git a/src/lib/rpc/bestRpcTracker.ts b/src/lib/rpc/bestRpcTracker.ts new file mode 100644 index 0000000000..3808625328 --- /dev/null +++ b/src/lib/rpc/bestRpcTracker.ts @@ -0,0 +1,397 @@ +import { Provider, ethers } from "ethers"; +import { + RPC_PROVIDERS, + SUPPORTED_CHAIN_IDS, + ARBITRUM, + AVALANCHE, + AVALANCHE_FUJI, + getFallbackRpcUrl, +} from "config/chains"; +import { getRpcProviderKey } from "config/localStorage"; +import { isDebugMode } from "lib/localStorage"; +import entries from "lodash/entries"; +import orderBy from "lodash/orderBy"; +import minBy from "lodash/minBy"; +import { differenceInMilliseconds } from "date-fns"; +import { getMulticallContract, getDataStoreContract } from "config/contracts"; +import { getContract } from "config/contracts"; +import { HASHED_MARKET_CONFIG_KEYS } from "prebuilt"; +import { getIsFlagEnabled } from "config/ab"; +import { sleep } from "lib/sleep"; +import sample from "lodash/sample"; +import { useEffect, useState } from "react"; +import { getStaticOracleKeeperFetcher } from "lib/oracleKeeperFetcher"; +import { getAbFlags } from "config/ab"; +import { getProviderNameFromUrl } from "lib/rpc/getProviderNameFromUrl"; + +const PROBE_INTERVAL = 10 * 1000; // 10 seconds / Frequency of RPC probing +const PROBE_FAIL_TIMEOUT = 10 * 1000; // 10 seconds / Abort RPC probe if it takes longer +const STORAGE_EXPIRE_TIMEOUT = 5 * 60 * 1000; // 5 minutes / Time after which provider saved in the localStorage is considered stale +const DISABLE_UNUSED_TRACKING_TIMEOUT = 1 * 60 * 1000; // 1 minute / Pause probing if no requests for the best RPC for this time + +const BLOCK_FROM_FUTURE_THRESHOLD = 1000; // Omit RPC if block number is higher than average on this value +const BLOCK_LAGGING_THRESHOLD = 50; // Omit RPC if block number is lower than highest valid on this value + +const RPC_TRACKER_UPDATE_EVENT = "rpc-tracker-update-event"; + +// DataStore field used for probing +const PROBE_SAMPLE_FIELD = "minCollateralFactor"; +// Markets used for `PROBE_SAMPLE_FIELD` reading +const PROBE_SAMPLE_MARKET = { + [ARBITRUM]: "0x70d95587d40A2caf56bd97485aB3Eec10Bee6336", // ETH/USD + [AVALANCHE]: "0xB7e69749E3d2EDd90ea59A4932EFEa2D41E245d7", // ETH/USD + [AVALANCHE_FUJI]: "0xbf338a6C595f06B7Cfff2FA8c958d49201466374", // ETH/USD +}; + +type ProbeData = { + url: string; + isSuccess: boolean; + responseTime: number | null; + blockNumber: number | null; + timestamp: Date; +}; + +type ProviderData = { + url: string; + provider: Provider; +}; + +type RpcTrackerState = { + [chainId: number]: { + lastUsage: Date | null; + currentBestProviderUrl: string; + providers: { + [providerUrl: string]: ProviderData; + }; + }; +}; + +let trackingIntervalId: number | null = null; +let trackerState: RpcTrackerState | null = null; + +function initRpcTracking() { + trackerState = initTrackerState(); + + if (trackingIntervalId) { + clearInterval(trackingIntervalId); + } + measureRpcData({ warmUp: true }); + trackingIntervalId = window.setInterval(() => measureRpcData(), PROBE_INTERVAL); +} + +function measureRpcData({ warmUp = false } = {}) { + if (!trackerState) { + throw new Error("RPC tracker state is not initialized"); + } + + entries(trackerState).forEach(async ([chainIdRaw, chainTracker]) => { + const chainId = Number(chainIdRaw); + const providers = Object.values(chainTracker.providers); + + const isUnusedChain = + !chainTracker.lastUsage || + differenceInMilliseconds(Date.now(), chainTracker.lastUsage) > DISABLE_UNUSED_TRACKING_TIMEOUT; + const isTrackingEnabled = (warmUp || !isUnusedChain) && providers.length > 1; + + if (!isTrackingEnabled) { + return; + } + + const probePromises = providers.map((providerInfo) => { + return probeRpc(chainId, providerInfo.provider, providerInfo.url); + }); + + const probeResults = await Promise.all(probePromises); + const successProbeResults = probeResults.filter((probe) => probe.isSuccess); + + if (!successProbeResults.length) { + setCurrentProvider(chainId, getFallbackRpcUrl(chainId)); + + return; + } + + const probeResultsByBlockNumber = orderBy(successProbeResults, ["blockNumber"], ["desc"]); + let bestBlockNumberProbe = probeResultsByBlockNumber[0]; + const secondBlockNumberProbe = probeResultsByBlockNumber[1]; + + // Rare case when RPC returned a block number from the future + if ( + bestBlockNumberProbe?.blockNumber && + secondBlockNumberProbe?.blockNumber && + bestBlockNumberProbe.blockNumber - secondBlockNumberProbe.blockNumber > BLOCK_FROM_FUTURE_THRESHOLD + ) { + bestBlockNumberProbe.isSuccess = false; + bestBlockNumberProbe = secondBlockNumberProbe; + } + + const probeStats = probeResultsByBlockNumber.map((probe) => { + let isValid = probe.isSuccess; + + const bestBlockNumber = bestBlockNumberProbe.blockNumber; + const currProbeBlockNumber = probe.blockNumber; + + // If the block number is lagging behind the best one + if (bestBlockNumber && currProbeBlockNumber && bestBlockNumber - currProbeBlockNumber > BLOCK_LAGGING_THRESHOLD) { + isValid = false; + } + + return { + ...probe, + isValid, + }; + }); + + const bestResponseTimeValidProbe = minBy( + probeStats.filter((probe) => probe.isValid), + "responseTime" + ); + + if (bestResponseTimeValidProbe) { + setCurrentProvider(chainId, bestResponseTimeValidProbe.url); + } + + if (isDebugMode()) { + // eslint-disable-next-line no-console + console.table( + orderBy( + probeStats.map((probe) => ({ + url: probe.url, + isSelected: probe.url === bestResponseTimeValidProbe?.url ? "✅" : "", + isValid: probe.isValid ? "✅" : "❌", + responseTime: probe.responseTime, + blockNumber: probe.blockNumber, + })), + ["responseTime"], + ["asc"] + ) + ); + } + }); +} + +function setCurrentProvider(chainId: number, newProviderUrl: string) { + if (!trackerState) { + throw new Error("RPC tracker state is not initialized"); + } + + trackerState[chainId].currentBestProviderUrl = newProviderUrl; + + window.dispatchEvent(new CustomEvent(RPC_TRACKER_UPDATE_EVENT)); + + getStaticOracleKeeperFetcher(chainId).fetchPostCounter({ + event: "rpcTracker.ranking.setBestRpc", + customFields: { + rpcProvider: getProviderNameFromUrl(newProviderUrl), + }, + abFlags: getAbFlags(), + }); + + const storageKey = JSON.stringify(getRpcProviderKey(chainId)); + + localStorage.setItem( + storageKey, + JSON.stringify({ + rpcUrl: newProviderUrl, + timestamp: Date.now(), + }) + ); +} + +async function probeRpc(chainId: number, provider: Provider, providerUrl: string): Promise { + const controller = new AbortController(); + + let responseTime: number | null = null; + + return await Promise.race([ + sleep(PROBE_FAIL_TIMEOUT).then(() => { + controller.abort(); + throw new Error("Probe timeout"); + }), + (async function callRpc() { + const dataStoreAddress = getContract(chainId, "DataStore"); + const multicallAddress = getContract(chainId, "Multicall"); + const dataStore = getDataStoreContract(chainId, provider); + const multicall = getMulticallContract(chainId, provider); + + const probeMarketAddress = PROBE_SAMPLE_MARKET[chainId]; + const probeFieldKey = HASHED_MARKET_CONFIG_KEYS[chainId]?.[probeMarketAddress]?.[PROBE_SAMPLE_FIELD]; + + let blockNumber: number | null = null; + let isSuccess = false; + + if (!dataStoreAddress || !multicallAddress || !probeMarketAddress || !probeFieldKey || !dataStore || !multicall) { + throw new Error("Failed to get RPC probe request data"); + } else { + const dataStoreData = dataStore.interface.encodeFunctionData("getUint", [probeFieldKey]); + const multicallData = multicall.interface.encodeFunctionData("blockAndAggregate", [ + [{ target: dataStoreAddress, callData: dataStoreData }], + ]); + + const body = { + jsonrpc: "2.0", + id: 1, + method: "eth_call", + params: [ + { + to: multicallAddress, + data: multicallData, + }, + "latest", + ], + }; + + try { + const startTime = Date.now(); + + const response = await fetch(providerUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + signal: controller.signal, + }); + + responseTime = Date.now() - startTime; + + if (!response.ok) { + throw new Error(`Network response was not ok: ${response.statusText}`); + } + + const { result } = await response.json(); + + if (result) { + const multicallResult = multicall.interface.decodeFunctionResult("blockAndAggregate", result); + const [sampleFieldValue] = dataStore.interface.decodeFunctionResult( + "getUint", + multicallResult.returnData[0].returnData + ); + + blockNumber = Number(multicallResult.blockNumber); + isSuccess = sampleFieldValue && sampleFieldValue > 0; + } + } catch (error) { + if (error.name !== "AbortError") { + throw error; + } + } + } + + return { + url: providerUrl, + responseTime, + blockNumber, + timestamp: new Date(), + isSuccess, + }; + })(), + ]).catch(() => { + return { + url: providerUrl, + responseTime: null, + blockNumber: null, + timestamp: new Date(), + isSuccess: false, + }; + }); +} + +function initTrackerState() { + const now = Date.now(); + + return SUPPORTED_CHAIN_IDS.reduce((acc, chainId) => { + const providersList = RPC_PROVIDERS[chainId] as string[]; + const providers = providersList.reduce>((acc, rpcUrl) => { + acc[rpcUrl] = { + url: rpcUrl, + provider: new ethers.JsonRpcProvider(rpcUrl), + }; + + return acc; + }, {}); + + let currentBestProviderUrl: string = RPC_PROVIDERS[chainId][0]; + + const storageKey = JSON.stringify(getRpcProviderKey(chainId)); + const storedProviderData = localStorage.getItem(storageKey); + + if (storedProviderData) { + let rpcUrl: string | undefined; + let timestamp: number | undefined; + + try { + const storedProvider = JSON.parse(storedProviderData); + + rpcUrl = storedProvider.rpcUrl; + timestamp = storedProvider.timestamp; + } catch (e) { + // eslint-disable-next-line no-console + console.error(`Failed to parse stored rpc provider data from \`${storageKey}\``, e); + } + + if (rpcUrl && providers[rpcUrl] && timestamp && now - timestamp < STORAGE_EXPIRE_TIMEOUT) { + currentBestProviderUrl = rpcUrl; + } + } + + acc[chainId] = { + lastUsage: null, + currentBestProviderUrl, + providers, + }; + + return acc; + }, {}); +} + +export function getBestRpcUrl(chainId: number) { + if (!getIsFlagEnabled("testSmartRpcSwitching")) { + return RPC_PROVIDERS[chainId][0] as string; + } + + if (!trackerState) { + initRpcTracking(); + } + + if (!trackerState) { + throw new Error("RPC tracker state is not initialized"); + } + + if (!trackerState[chainId]) { + if (RPC_PROVIDERS[chainId]?.length) { + return sample(RPC_PROVIDERS[chainId]); + } + + throw new Error(`No RPC providers found for chainId: ${chainId}`); + } + + trackerState[chainId].lastUsage = new Date(); + + return trackerState[chainId].currentBestProviderUrl; +} + +export function useBestRpcUrl(chainId: number) { + const [bestRpcUrl, setBestRpcUrl] = useState(() => getBestRpcUrl(chainId)); + + useEffect(() => { + let isMounted = true; + + setBestRpcUrl(getBestRpcUrl(chainId)); + + function handleRpcUpdate() { + if (isMounted) { + const newRpcUrl = getBestRpcUrl(chainId); + setBestRpcUrl(newRpcUrl); + } + } + + window.addEventListener(RPC_TRACKER_UPDATE_EVENT, handleRpcUpdate); + + return () => { + isMounted = false; + window.removeEventListener(RPC_TRACKER_UPDATE_EVENT, handleRpcUpdate); + }; + }, [chainId]); + + return bestRpcUrl; +} diff --git a/src/lib/rpc/getProviderNameFromUrl.ts b/src/lib/rpc/getProviderNameFromUrl.ts new file mode 100644 index 0000000000..63b719ab27 --- /dev/null +++ b/src/lib/rpc/getProviderNameFromUrl.ts @@ -0,0 +1,18 @@ +export function getProviderNameFromUrl(rpcUrl: string) { + let rpcName = "unknown"; + + try { + const parsedUrl = new URL(rpcUrl); + const hostnameParts = parsedUrl.hostname.split("."); + const rpcDomain = hostnameParts.at(-2); + + if (rpcDomain) { + rpcName = rpcDomain; + } + } catch (error) { + // eslint-disable-next-line no-console + console.error(`Invalid rpc URL: ${rpcUrl}`); + } + + return rpcName; +} diff --git a/src/lib/rpc/index.ts b/src/lib/rpc/index.ts index 76d1beae45..fd37a75fe2 100644 --- a/src/lib/rpc/index.ts +++ b/src/lib/rpc/index.ts @@ -7,11 +7,11 @@ import { FALLBACK_PROVIDERS, getAlchemyArbitrumWsUrl, getFallbackRpcUrl, - getRpcUrl, } from "config/chains"; import { Signer, ethers } from "ethers"; import { useEffect, useState } from "react"; import { isDevelopment } from "config/env"; +import { getBestRpcUrl, useBestRpcUrl } from "lib/rpc/bestRpcTracker"; export function getProvider(signer: undefined, chainId: number): ethers.JsonRpcProvider; export function getProvider(signer: Signer, chainId: number): Signer; @@ -23,7 +23,7 @@ export function getProvider(signer: Signer | undefined, chainId: number): ethers return signer; } - url = getRpcUrl(chainId); + url = getBestRpcUrl(chainId); const network = Network.from(chainId); @@ -48,7 +48,9 @@ export function getWsProvider(chainId: number): WebSocketProvider | JsonRpcProvi } if (chainId === AVALANCHE_FUJI) { - const provider = new ethers.JsonRpcProvider(getRpcUrl(AVALANCHE_FUJI), network, { staticNetwork: network }); + const provider = new ethers.JsonRpcProvider(getBestRpcUrl(AVALANCHE_FUJI), network, { + staticNetwork: network, + }); provider.pollingInterval = 2000; return provider; } @@ -59,9 +61,9 @@ export function getFallbackProvider(chainId: number) { return; } - const provider = getFallbackRpcUrl(chainId); + const providerUrl = getFallbackRpcUrl(chainId); - return new ethers.JsonRpcProvider(provider, chainId, { + return new ethers.JsonRpcProvider(providerUrl, chainId, { staticNetwork: Network.from(chainId), }); } @@ -69,10 +71,10 @@ export function getFallbackProvider(chainId: number) { export function useJsonRpcProvider(chainId: number) { const [provider, setProvider] = useState(); + const rpcUrl = useBestRpcUrl(chainId); + useEffect(() => { async function initializeProvider() { - const rpcUrl = getRpcUrl(chainId); - if (!rpcUrl) return; const provider = new ethers.JsonRpcProvider(rpcUrl, chainId); @@ -84,7 +86,7 @@ export function useJsonRpcProvider(chainId: number) { } initializeProvider(); - }, [chainId]); + }, [chainId, rpcUrl]); return { provider }; }