From 4de4efc6354ee335ad1445a7ee2c9436d9fb668a Mon Sep 17 00:00:00 2001 From: Connor Prussin Date: Mon, 27 Oct 2025 17:14:02 -0700 Subject: [PATCH] fix(insights): use web3.js Connection instead of PythConnection Upon investigation, it seems the `PythConnection` from `@pythnetwork/client` is extremely inefficient for a few reasons: 1. `PythConnection` requires loading _all_ account info when creating the connection class, so that it can build a mapping from price account to program account. However IH never used this mapping in practice, so all of that was wasted work. This caused major issues for page load performance as loading all account info from pythnet was a ton of parsing which locked up the browser for multiple seconds. 2. `PythConnection` did not expose a mechanism to remove unused subscriptions In doing this I also removed all the live prices contexts since none of that is needed any more, as well as the call to initialize prices with the last available price as that too looks unnecessary and redundant. This change should _drastically_ improve performance in IH, especially during page load. --- apps/insights/src/components/Root/index.tsx | 3 +- .../src/hooks/use-live-price-data.tsx | 180 ++---------------- apps/insights/src/services/pyth/index.ts | 33 ++-- 3 files changed, 38 insertions(+), 178 deletions(-) diff --git a/apps/insights/src/components/Root/index.tsx b/apps/insights/src/components/Root/index.tsx index 35954de1a0..e3b708356a 100644 --- a/apps/insights/src/components/Root/index.tsx +++ b/apps/insights/src/components/Root/index.tsx @@ -10,7 +10,6 @@ import { GOOGLE_ANALYTICS_ID, } from "../../config/server"; import { getPublishersWithRankings } from "../../get-publishers-with-rankings"; -import { LivePriceDataProvider } from "../../hooks/use-live-price-data"; import { Cluster } from "../../services/pyth"; import { getFeeds } from "../../services/pyth/get-feeds"; import { PriceFeedIcon } from "../PriceFeedIcon"; @@ -32,7 +31,7 @@ export const Root = ({ children }: Props) => ( amplitudeApiKey={AMPLITUDE_API_KEY} googleAnalyticsId={GOOGLE_ANALYTICS_ID} enableAccessibilityReporting={ENABLE_ACCESSIBILITY_REPORTING} - providers={[NuqsAdapter, LivePriceDataProvider]} + providers={[NuqsAdapter]} tabs={TABS} extraCta={} > diff --git a/apps/insights/src/hooks/use-live-price-data.tsx b/apps/insights/src/hooks/use-live-price-data.tsx index 2ebbb2169f..b47391defe 100644 --- a/apps/insights/src/hooks/use-live-price-data.tsx +++ b/apps/insights/src/hooks/use-live-price-data.tsx @@ -3,53 +3,34 @@ import type { PriceData } from "@pythnetwork/client"; import { useLogger } from "@pythnetwork/component-library/useLogger"; import { PublicKey } from "@solana/web3.js"; -import type { ComponentProps } from "react"; -import { - use, - createContext, - useEffect, - useCallback, - useState, - useMemo, - useRef, -} from "react"; +import { useEffect, useState, useMemo } from "react"; -import { - Cluster, - subscribe, - getAssetPricesFromAccounts, -} from "../services/pyth"; - -const LivePriceDataContext = createContext< - ReturnType | undefined ->(undefined); - -type LivePriceDataProviderProps = Omit< - ComponentProps, - "value" ->; - -export const LivePriceDataProvider = (props: LivePriceDataProviderProps) => { - const priceData = usePriceData(); - - return ; -}; +import { Cluster, subscribe, unsubscribe } from "../services/pyth"; export const useLivePriceData = (cluster: Cluster, feedKey: string) => { - const { addSubscription, removeSubscription } = - useLivePriceDataContext()[cluster]; - + const logger = useLogger(); const [data, setData] = useState<{ current: PriceData | undefined; prev: PriceData | undefined; }>({ current: undefined, prev: undefined }); useEffect(() => { - addSubscription(feedKey, setData); + const subscriptionId = subscribe( + cluster, + new PublicKey(feedKey), + ({ data }) => { + setData((prev) => ({ current: data, prev: prev.current })); + }, + ); return () => { - removeSubscription(feedKey, setData); + unsubscribe(cluster, subscriptionId).catch((error: unknown) => { + logger.error( + `Failed to remove subscription for price feed ${feedKey}`, + error, + ); + }); }; - }, [addSubscription, removeSubscription, feedKey]); + }, [cluster, feedKey, logger]); return data; }; @@ -75,130 +56,3 @@ export const useLivePriceComponent = ( exponent: current?.exponent, }; }; - -const usePriceData = () => { - const pythnetPriceData = usePriceDataForCluster(Cluster.Pythnet); - const pythtestPriceData = usePriceDataForCluster(Cluster.PythtestConformance); - - return { - [Cluster.Pythnet]: pythnetPriceData, - [Cluster.PythtestConformance]: pythtestPriceData, - }; -}; - -type Subscription = (value: { - current: PriceData; - prev: PriceData | undefined; -}) => void; - -const usePriceDataForCluster = (cluster: Cluster) => { - const [feedKeys, setFeedKeys] = useState([]); - const feedSubscriptions = useRef>>(new Map()); - const priceData = useRef>(new Map()); - const prevPriceData = useRef>(new Map()); - const logger = useLogger(); - - useEffect(() => { - // First, we initialize prices with the last available price. This way, if - // there's any symbol that isn't currently publishing prices (e.g. the - // markets are closed), we will still display the last published price for - // that symbol. - const uninitializedFeedKeys = feedKeys.filter( - (key) => !priceData.current.has(key), - ); - if (uninitializedFeedKeys.length > 0) { - getAssetPricesFromAccounts( - cluster, - uninitializedFeedKeys.map((key) => new PublicKey(key)), - ) - .then((initialPrices) => { - for (const [i, price] of initialPrices.entries()) { - const key = uninitializedFeedKeys[i]; - if (key && !priceData.current.has(key)) { - priceData.current.set(key, price); - } - } - }) - .catch((error: unknown) => { - logger.error("Failed to fetch initial prices", error); - }); - } - - // Then, we create a subscription to update prices live. - const connection = subscribe( - cluster, - feedKeys.map((key) => new PublicKey(key)), - ({ price_account }, data) => { - if (price_account) { - const prevData = priceData.current.get(price_account); - if (prevData) { - prevPriceData.current.set(price_account, prevData); - } - priceData.current.set(price_account, data); - for (const subscription of feedSubscriptions.current.get( - price_account, - ) ?? []) { - subscription({ current: data, prev: prevData }); - } - } - }, - ); - - connection.start().catch((error: unknown) => { - logger.error("Failed to subscribe to prices", error); - }); - return () => { - connection.stop().catch((error: unknown) => { - logger.error("Failed to unsubscribe from price updates", error); - }); - }; - }, [feedKeys, logger, cluster]); - - const addSubscription = useCallback( - (key: string, subscription: Subscription) => { - const current = feedSubscriptions.current.get(key); - if (current === undefined) { - feedSubscriptions.current.set(key, new Set([subscription])); - setFeedKeys((prev) => [...new Set([...prev, key])]); - } else { - current.add(subscription); - } - }, - [feedSubscriptions], - ); - - const removeSubscription = useCallback( - (key: string, subscription: Subscription) => { - const current = feedSubscriptions.current.get(key); - if (current) { - if (current.size === 0) { - feedSubscriptions.current.delete(key); - setFeedKeys((prev) => prev.filter((elem) => elem !== key)); - } else { - current.delete(subscription); - } - } - }, - [feedSubscriptions], - ); - - return { - addSubscription, - removeSubscription, - }; -}; - -const useLivePriceDataContext = () => { - const prices = use(LivePriceDataContext); - if (prices === undefined) { - throw new LivePriceDataProviderNotInitializedError(); - } - return prices; -}; - -class LivePriceDataProviderNotInitializedError extends Error { - constructor() { - super("This component must be a child of "); - this.name = "LivePriceDataProviderNotInitializedError"; - } -} diff --git a/apps/insights/src/services/pyth/index.ts b/apps/insights/src/services/pyth/index.ts index 54de1966fa..f872f2d33e 100644 --- a/apps/insights/src/services/pyth/index.ts +++ b/apps/insights/src/services/pyth/index.ts @@ -1,9 +1,10 @@ +import type { PriceData } from "@pythnetwork/client"; import { PythHttpClient, - PythConnection, getPythProgramKeyForCluster, + parsePriceData, } from "@pythnetwork/client"; -import type { PythPriceCallback } from "@pythnetwork/client/lib/PythConnection"; +import type { AccountInfo } from "@solana/web3.js"; import { Connection, PublicKey } from "@solana/web3.js"; import { PYTHNET_RPC, PYTHTEST_CONFORMANCE_RPC } from "../../config/isomorphic"; @@ -67,15 +68,21 @@ export const getAssetPricesFromAccounts = ( export const subscribe = ( cluster: Cluster, - feeds: PublicKey[], - cb: PythPriceCallback, -) => { - const pythConn = new PythConnection( - connections[cluster], - getPythProgramKeyForCluster(ClusterToName[cluster]), - "confirmed", - feeds, + feed: PublicKey, + cb: (values: { accountInfo: AccountInfo; data: PriceData }) => void, +) => + connections[cluster].onAccountChange( + feed, + (accountInfo, context) => { + cb({ + accountInfo, + data: parsePriceData(accountInfo.data, context.slot), + }); + }, + { + commitment: "confirmed", + }, ); - pythConn.onPriceChange(cb); - return pythConn; -}; + +export const unsubscribe = (cluster: Cluster, subscriptionId: number) => + connections[cluster].removeAccountChangeListener(subscriptionId);