33import type { PriceData } from "@pythnetwork/client" ;
44import { useLogger } from "@pythnetwork/component-library/useLogger" ;
55import { PublicKey } from "@solana/web3.js" ;
6- import type { ComponentProps } from "react" ;
7- import {
8- use ,
9- createContext ,
10- useEffect ,
11- useCallback ,
12- useState ,
13- useMemo ,
14- useRef ,
15- } from "react" ;
6+ import { useEffect , useState , useMemo } from "react" ;
167
17- import {
18- Cluster ,
19- subscribe ,
20- getAssetPricesFromAccounts ,
21- } from "../services/pyth" ;
22-
23- const LivePriceDataContext = createContext <
24- ReturnType < typeof usePriceData > | undefined
25- > ( undefined ) ;
26-
27- type LivePriceDataProviderProps = Omit <
28- ComponentProps < typeof LivePriceDataContext > ,
29- "value"
30- > ;
31-
32- export const LivePriceDataProvider = ( props : LivePriceDataProviderProps ) => {
33- const priceData = usePriceData ( ) ;
34-
35- return < LivePriceDataContext value = { priceData } { ...props } /> ;
36- } ;
8+ import { Cluster , subscribe , unsubscribe } from "../services/pyth" ;
379
3810export const useLivePriceData = ( cluster : Cluster , feedKey : string ) => {
39- const { addSubscription, removeSubscription } =
40- useLivePriceDataContext ( ) [ cluster ] ;
41-
11+ const logger = useLogger ( ) ;
4212 const [ data , setData ] = useState < {
4313 current : PriceData | undefined ;
4414 prev : PriceData | undefined ;
4515 } > ( { current : undefined , prev : undefined } ) ;
4616
4717 useEffect ( ( ) => {
48- addSubscription ( feedKey , setData ) ;
18+ const subscriptionId = subscribe (
19+ cluster ,
20+ new PublicKey ( feedKey ) ,
21+ ( { data } ) => {
22+ setData ( ( prev ) => ( { current : data , prev : prev . current } ) ) ;
23+ } ,
24+ ) ;
4925 return ( ) => {
50- removeSubscription ( feedKey , setData ) ;
26+ unsubscribe ( cluster , subscriptionId ) . catch ( ( error : unknown ) => {
27+ logger . error (
28+ `Failed to remove subscription for price feed ${ feedKey } ` ,
29+ error ,
30+ ) ;
31+ } ) ;
5132 } ;
52- } , [ addSubscription , removeSubscription , feedKey ] ) ;
33+ } , [ cluster , feedKey , logger ] ) ;
5334
5435 return data ;
5536} ;
@@ -75,130 +56,3 @@ export const useLivePriceComponent = (
7556 exponent : current ?. exponent ,
7657 } ;
7758} ;
78-
79- const usePriceData = ( ) => {
80- const pythnetPriceData = usePriceDataForCluster ( Cluster . Pythnet ) ;
81- const pythtestPriceData = usePriceDataForCluster ( Cluster . PythtestConformance ) ;
82-
83- return {
84- [ Cluster . Pythnet ] : pythnetPriceData ,
85- [ Cluster . PythtestConformance ] : pythtestPriceData ,
86- } ;
87- } ;
88-
89- type Subscription = ( value : {
90- current : PriceData ;
91- prev : PriceData | undefined ;
92- } ) => void ;
93-
94- const usePriceDataForCluster = ( cluster : Cluster ) => {
95- const [ feedKeys , setFeedKeys ] = useState < string [ ] > ( [ ] ) ;
96- const feedSubscriptions = useRef < Map < string , Set < Subscription > > > ( new Map ( ) ) ;
97- const priceData = useRef < Map < string , PriceData > > ( new Map ( ) ) ;
98- const prevPriceData = useRef < Map < string , PriceData > > ( new Map ( ) ) ;
99- const logger = useLogger ( ) ;
100-
101- useEffect ( ( ) => {
102- // First, we initialize prices with the last available price. This way, if
103- // there's any symbol that isn't currently publishing prices (e.g. the
104- // markets are closed), we will still display the last published price for
105- // that symbol.
106- const uninitializedFeedKeys = feedKeys . filter (
107- ( key ) => ! priceData . current . has ( key ) ,
108- ) ;
109- if ( uninitializedFeedKeys . length > 0 ) {
110- getAssetPricesFromAccounts (
111- cluster ,
112- uninitializedFeedKeys . map ( ( key ) => new PublicKey ( key ) ) ,
113- )
114- . then ( ( initialPrices ) => {
115- for ( const [ i , price ] of initialPrices . entries ( ) ) {
116- const key = uninitializedFeedKeys [ i ] ;
117- if ( key && ! priceData . current . has ( key ) ) {
118- priceData . current . set ( key , price ) ;
119- }
120- }
121- } )
122- . catch ( ( error : unknown ) => {
123- logger . error ( "Failed to fetch initial prices" , error ) ;
124- } ) ;
125- }
126-
127- // Then, we create a subscription to update prices live.
128- const connection = subscribe (
129- cluster ,
130- feedKeys . map ( ( key ) => new PublicKey ( key ) ) ,
131- ( { price_account } , data ) => {
132- if ( price_account ) {
133- const prevData = priceData . current . get ( price_account ) ;
134- if ( prevData ) {
135- prevPriceData . current . set ( price_account , prevData ) ;
136- }
137- priceData . current . set ( price_account , data ) ;
138- for ( const subscription of feedSubscriptions . current . get (
139- price_account ,
140- ) ?? [ ] ) {
141- subscription ( { current : data , prev : prevData } ) ;
142- }
143- }
144- } ,
145- ) ;
146-
147- connection . start ( ) . catch ( ( error : unknown ) => {
148- logger . error ( "Failed to subscribe to prices" , error ) ;
149- } ) ;
150- return ( ) => {
151- connection . stop ( ) . catch ( ( error : unknown ) => {
152- logger . error ( "Failed to unsubscribe from price updates" , error ) ;
153- } ) ;
154- } ;
155- } , [ feedKeys , logger , cluster ] ) ;
156-
157- const addSubscription = useCallback (
158- ( key : string , subscription : Subscription ) => {
159- const current = feedSubscriptions . current . get ( key ) ;
160- if ( current === undefined ) {
161- feedSubscriptions . current . set ( key , new Set ( [ subscription ] ) ) ;
162- setFeedKeys ( ( prev ) => [ ...new Set ( [ ...prev , key ] ) ] ) ;
163- } else {
164- current . add ( subscription ) ;
165- }
166- } ,
167- [ feedSubscriptions ] ,
168- ) ;
169-
170- const removeSubscription = useCallback (
171- ( key : string , subscription : Subscription ) => {
172- const current = feedSubscriptions . current . get ( key ) ;
173- if ( current ) {
174- if ( current . size === 0 ) {
175- feedSubscriptions . current . delete ( key ) ;
176- setFeedKeys ( ( prev ) => prev . filter ( ( elem ) => elem !== key ) ) ;
177- } else {
178- current . delete ( subscription ) ;
179- }
180- }
181- } ,
182- [ feedSubscriptions ] ,
183- ) ;
184-
185- return {
186- addSubscription,
187- removeSubscription,
188- } ;
189- } ;
190-
191- const useLivePriceDataContext = ( ) => {
192- const prices = use ( LivePriceDataContext ) ;
193- if ( prices === undefined ) {
194- throw new LivePriceDataProviderNotInitializedError ( ) ;
195- }
196- return prices ;
197- } ;
198-
199- class LivePriceDataProviderNotInitializedError extends Error {
200- constructor ( ) {
201- super ( "This component must be a child of <LivePriceDataProvider>" ) ;
202- this . name = "LivePriceDataProviderNotInitializedError" ;
203- }
204- }
0 commit comments