|
@@ -3,53 +3,34 @@
|
|
|
import type { PriceData } from "@pythnetwork/client";
|
|
import type { PriceData } from "@pythnetwork/client";
|
|
|
import { useLogger } from "@pythnetwork/component-library/useLogger";
|
|
import { useLogger } from "@pythnetwork/component-library/useLogger";
|
|
|
import { PublicKey } from "@solana/web3.js";
|
|
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) => {
|
|
export const useLivePriceData = (cluster: Cluster, feedKey: string) => {
|
|
|
- const { addSubscription, removeSubscription } =
|
|
|
|
|
- useLivePriceDataContext()[cluster];
|
|
|
|
|
-
|
|
|
|
|
|
|
+ const logger = useLogger();
|
|
|
const [data, setData] = useState<{
|
|
const [data, setData] = useState<{
|
|
|
current: PriceData | undefined;
|
|
current: PriceData | undefined;
|
|
|
prev: PriceData | undefined;
|
|
prev: PriceData | undefined;
|
|
|
}>({ current: undefined, prev: undefined });
|
|
}>({ current: undefined, prev: undefined });
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
- addSubscription(feedKey, setData);
|
|
|
|
|
|
|
+ const subscriptionId = subscribe(
|
|
|
|
|
+ cluster,
|
|
|
|
|
+ new PublicKey(feedKey),
|
|
|
|
|
+ ({ data }) => {
|
|
|
|
|
+ setData((prev) => ({ current: data, prev: prev.current }));
|
|
|
|
|
+ },
|
|
|
|
|
+ );
|
|
|
return () => {
|
|
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;
|
|
return data;
|
|
|
};
|
|
};
|
|
@@ -75,130 +56,3 @@ export const useLivePriceComponent = (
|
|
|
exponent: current?.exponent,
|
|
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";
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|