Browse Source

Merge pull request #3158 from pyth-network/cprussin/dont-use-pythconnection

fix(insights): use web3.js Connection instead of PythConnection
Connor Prussin 3 weeks ago
parent
commit
331c14efdb

+ 1 - 2
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={<SearchButton />}
   >

+ 17 - 163
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<typeof usePriceData> | undefined
->(undefined);
-
-type LivePriceDataProviderProps = Omit<
-  ComponentProps<typeof LivePriceDataContext>,
-  "value"
->;
-
-export const LivePriceDataProvider = (props: LivePriceDataProviderProps) => {
-  const priceData = usePriceData();
-
-  return <LivePriceDataContext value={priceData} {...props} />;
-};
+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<string[]>([]);
-  const feedSubscriptions = useRef<Map<string, Set<Subscription>>>(new Map());
-  const priceData = useRef<Map<string, PriceData>>(new Map());
-  const prevPriceData = useRef<Map<string, PriceData>>(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 <LivePriceDataProvider>");
-    this.name = "LivePriceDataProviderNotInitializedError";
-  }
-}

+ 20 - 13
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<Buffer>; 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);