1
1
"use client" ;
2
2
3
3
import { PlusMinus } from "@phosphor-icons/react/dist/ssr/PlusMinus" ;
4
- import { useLogger } from "@pythnetwork/app-logger" ;
5
4
import type { PriceData , PriceComponent } from "@pythnetwork/client" ;
6
5
import { Skeleton } from "@pythnetwork/component-library/Skeleton" ;
7
- import { useMap } from "@react-hookz/web" ;
8
- import { PublicKey } from "@solana/web3.js" ;
9
- import {
10
- type ComponentProps ,
11
- type ReactNode ,
12
- use ,
13
- createContext ,
14
- useEffect ,
15
- useCallback ,
16
- useState ,
17
- useMemo ,
18
- } from "react" ;
6
+ import { type ReactNode , useMemo } from "react" ;
19
7
import { useNumberFormatter , useDateFormatter } from "react-aria" ;
20
8
21
9
import styles from "./index.module.scss" ;
22
10
import {
23
- Cluster ,
24
- subscribe ,
25
- getAssetPricesFromAccounts ,
26
- } from "../../services/pyth" ;
11
+ useLivePriceComponent ,
12
+ useLivePriceData ,
13
+ } from "../../hooks/use-live-price-data" ;
27
14
28
15
export const SKELETON_WIDTH = 20 ;
29
16
30
- const LivePricesContext = createContext <
31
- ReturnType < typeof usePriceData > | undefined
32
- > ( undefined ) ;
33
-
34
- type LivePricesProviderProps = Omit <
35
- ComponentProps < typeof LivePricesContext > ,
36
- "value"
37
- > ;
38
-
39
- export const LivePricesProvider = ( props : LivePricesProviderProps ) => {
40
- const priceData = usePriceData ( ) ;
41
-
42
- return < LivePricesContext value = { priceData } { ...props } /> ;
43
- } ;
44
-
45
- export const useLivePrice = ( feedKey : string ) => {
46
- const { priceData, prevPriceData, addSubscription, removeSubscription } =
47
- useLivePrices ( ) ;
48
-
49
- useEffect ( ( ) => {
50
- addSubscription ( feedKey ) ;
51
- return ( ) => {
52
- removeSubscription ( feedKey ) ;
53
- } ;
54
- } , [ addSubscription , removeSubscription , feedKey ] ) ;
55
-
56
- const current = priceData . get ( feedKey ) ;
57
- const prev = prevPriceData . get ( feedKey ) ;
58
-
59
- return { current, prev } ;
60
- } ;
61
-
62
- export const useLivePriceComponent = (
63
- feedKey : string ,
64
- publisherKeyAsBase58 : string ,
65
- ) => {
66
- const { current, prev } = useLivePrice ( feedKey ) ;
67
- const publisherKey = useMemo (
68
- ( ) => new PublicKey ( publisherKeyAsBase58 ) ,
69
- [ publisherKeyAsBase58 ] ,
70
- ) ;
71
-
72
- return {
73
- current : current ?. priceComponents . find ( ( component ) =>
74
- component . publisher . equals ( publisherKey ) ,
75
- ) ,
76
- prev : prev ?. priceComponents . find ( ( component ) =>
77
- component . publisher . equals ( publisherKey ) ,
78
- ) ,
79
- } ;
80
- } ;
81
-
82
17
export const LivePrice = ( {
83
18
feedKey,
84
19
publisherKey,
@@ -93,7 +28,7 @@ export const LivePrice = ({
93
28
) ;
94
29
95
30
const LiveAggregatePrice = ( { feedKey } : { feedKey : string } ) => {
96
- const { prev, current } = useLivePrice ( feedKey ) ;
31
+ const { prev, current } = useLivePriceData ( feedKey ) ;
97
32
return (
98
33
< Price current = { current ?. aggregate . price } prev = { prev ?. aggregate . price } />
99
34
) ;
@@ -117,7 +52,7 @@ const Price = ({
117
52
prev ?: number | undefined ;
118
53
current ?: number | undefined ;
119
54
} ) => {
120
- const numberFormatter = useNumberFormatter ( { maximumSignificantDigits : 5 } ) ;
55
+ const numberFormatter = useNumberFormatter ( { maximumFractionDigits : 5 } ) ;
121
56
122
57
return current === undefined ? (
123
58
< Skeleton width = { SKELETON_WIDTH } />
@@ -145,7 +80,7 @@ export const LiveConfidence = ({
145
80
) ;
146
81
147
82
const LiveAggregateConfidence = ( { feedKey } : { feedKey : string } ) => {
148
- const { current } = useLivePrice ( feedKey ) ;
83
+ const { current } = useLivePriceData ( feedKey ) ;
149
84
return < Confidence confidence = { current ?. aggregate . confidence } /> ;
150
85
} ;
151
86
@@ -161,7 +96,7 @@ const LiveComponentConfidence = ({
161
96
} ;
162
97
163
98
const Confidence = ( { confidence } : { confidence ?: number | undefined } ) => {
164
- const numberFormatter = useNumberFormatter ( { maximumSignificantDigits : 5 } ) ;
99
+ const numberFormatter = useNumberFormatter ( { maximumFractionDigits : 5 } ) ;
165
100
166
101
return (
167
102
< span className = { styles . confidence } >
@@ -176,7 +111,7 @@ const Confidence = ({ confidence }: { confidence?: number | undefined }) => {
176
111
} ;
177
112
178
113
export const LiveLastUpdated = ( { feedKey } : { feedKey : string } ) => {
179
- const { current } = useLivePrice ( feedKey ) ;
114
+ const { current } = useLivePriceData ( feedKey ) ;
180
115
const formatterWithDate = useDateFormatter ( {
181
116
dateStyle : "short" ,
182
117
timeStyle : "medium" ,
@@ -209,7 +144,7 @@ export const LiveValue = <T extends keyof PriceData>({
209
144
field,
210
145
defaultValue,
211
146
} : LiveValueProps < T > ) => {
212
- const { current } = useLivePrice ( feedKey ) ;
147
+ const { current } = useLivePriceData ( feedKey ) ;
213
148
214
149
return current ?. [ field ] ?. toString ( ) ?? defaultValue ;
215
150
} ;
@@ -241,109 +176,6 @@ const isToday = (date: Date) => {
241
176
) ;
242
177
} ;
243
178
244
- const usePriceData = ( ) => {
245
- const feedSubscriptions = useMap < string , number > ( [ ] ) ;
246
- const [ feedKeys , setFeedKeys ] = useState < string [ ] > ( [ ] ) ;
247
- const prevPriceData = useMap < string , PriceData > ( [ ] ) ;
248
- const priceData = useMap < string , PriceData > ( [ ] ) ;
249
- const logger = useLogger ( ) ;
250
-
251
- useEffect ( ( ) => {
252
- // First, we initialize prices with the last available price. This way, if
253
- // there's any symbol that isn't currently publishing prices (e.g. the
254
- // markets are closed), we will still display the last published price for
255
- // that symbol.
256
- const uninitializedFeedKeys = feedKeys . filter ( ( key ) => ! priceData . has ( key ) ) ;
257
- if ( uninitializedFeedKeys . length > 0 ) {
258
- getAssetPricesFromAccounts (
259
- Cluster . Pythnet ,
260
- uninitializedFeedKeys . map ( ( key ) => new PublicKey ( key ) ) ,
261
- )
262
- . then ( ( initialPrices ) => {
263
- for ( const [ i , price ] of initialPrices . entries ( ) ) {
264
- const key = uninitializedFeedKeys [ i ] ;
265
- if ( key && ! priceData . has ( key ) ) {
266
- priceData . set ( key , price ) ;
267
- }
268
- }
269
- } )
270
- . catch ( ( error : unknown ) => {
271
- logger . error ( "Failed to fetch initial prices" , error ) ;
272
- } ) ;
273
- }
274
-
275
- // Then, we create a subscription to update prices live.
276
- const connection = subscribe (
277
- Cluster . Pythnet ,
278
- feedKeys . map ( ( key ) => new PublicKey ( key ) ) ,
279
- ( { price_account } , data ) => {
280
- if ( price_account ) {
281
- const prevData = priceData . get ( price_account ) ;
282
- if ( prevData ) {
283
- prevPriceData . set ( price_account , prevData ) ;
284
- }
285
- priceData . set ( price_account , data ) ;
286
- }
287
- } ,
288
- ) ;
289
-
290
- connection . start ( ) . catch ( ( error : unknown ) => {
291
- logger . error ( "Failed to subscribe to prices" , error ) ;
292
- } ) ;
293
- return ( ) => {
294
- connection . stop ( ) . catch ( ( error : unknown ) => {
295
- logger . error ( "Failed to unsubscribe from price updates" , error ) ;
296
- } ) ;
297
- } ;
298
- } , [ feedKeys , logger , priceData , prevPriceData ] ) ;
299
-
300
- const addSubscription = useCallback (
301
- ( key : string ) => {
302
- const current = feedSubscriptions . get ( key ) ?? 0 ;
303
- feedSubscriptions . set ( key , current + 1 ) ;
304
- if ( current === 0 ) {
305
- setFeedKeys ( ( prev ) => [ ...new Set ( [ ...prev , key ] ) ] ) ;
306
- }
307
- } ,
308
- [ feedSubscriptions ] ,
309
- ) ;
310
-
311
- const removeSubscription = useCallback (
312
- ( key : string ) => {
313
- const current = feedSubscriptions . get ( key ) ;
314
- if ( current ) {
315
- feedSubscriptions . set ( key , current - 1 ) ;
316
- if ( current === 1 ) {
317
- setFeedKeys ( ( prev ) => prev . filter ( ( elem ) => elem !== key ) ) ;
318
- }
319
- }
320
- } ,
321
- [ feedSubscriptions ] ,
322
- ) ;
323
-
324
- return {
325
- priceData : new Map ( priceData ) ,
326
- prevPriceData : new Map ( prevPriceData ) ,
327
- addSubscription,
328
- removeSubscription,
329
- } ;
330
- } ;
331
-
332
- const useLivePrices = ( ) => {
333
- const prices = use ( LivePricesContext ) ;
334
- if ( prices === undefined ) {
335
- throw new LivePricesProviderNotInitializedError ( ) ;
336
- }
337
- return prices ;
338
- } ;
339
-
340
- class LivePricesProviderNotInitializedError extends Error {
341
- constructor ( ) {
342
- super ( "This component must be a child of <LivePricesProvider>" ) ;
343
- this . name = "LivePricesProviderNotInitializedError" ;
344
- }
345
- }
346
-
347
179
const getChangeDirection = (
348
180
prevPrice : number | undefined ,
349
181
price : number ,
0 commit comments