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);