2
2
3
3
import { PlusMinus } from "@phosphor-icons/react/dist/ssr/PlusMinus" ;
4
4
import { useLogger } from "@pythnetwork/app-logger" ;
5
- import type { PriceData } from "@pythnetwork/client" ;
5
+ import type { PriceData , PriceComponent } from "@pythnetwork/client" ;
6
6
import { Skeleton } from "@pythnetwork/component-library/Skeleton" ;
7
7
import { useMap } from "@react-hookz/web" ;
8
8
import { PublicKey } from "@solana/web3.js" ;
@@ -19,20 +19,18 @@ import {
19
19
import { useNumberFormatter , useDateFormatter } from "react-aria" ;
20
20
21
21
import styles from "./index.module.scss" ;
22
- import { client , subscribe } from "../../services/pyth" ;
22
+ import {
23
+ Cluster ,
24
+ subscribe ,
25
+ getAssetPricesFromAccounts ,
26
+ } from "../../services/pyth" ;
23
27
24
28
export const SKELETON_WIDTH = 20 ;
25
29
26
30
const LivePricesContext = createContext <
27
31
ReturnType < typeof usePriceData > | undefined
28
32
> ( undefined ) ;
29
33
30
- type Price = PriceData & {
31
- direction : ChangeDirection ;
32
- } ;
33
-
34
- type ChangeDirection = "up" | "down" | "flat" ;
35
-
36
34
type LivePricesProviderProps = Omit <
37
35
ComponentProps < typeof LivePricesContext > ,
38
36
"value"
@@ -45,7 +43,8 @@ export const LivePricesProvider = (props: LivePricesProviderProps) => {
45
43
} ;
46
44
47
45
export const useLivePrice = ( feedKey : string ) => {
48
- const { priceData, addSubscription, removeSubscription } = useLivePrices ( ) ;
46
+ const { priceData, prevPriceData, addSubscription, removeSubscription } =
47
+ useLivePrices ( ) ;
49
48
50
49
useEffect ( ( ) => {
51
50
addSubscription ( feedKey ) ;
@@ -54,40 +53,130 @@ export const useLivePrice = (feedKey: string) => {
54
53
} ;
55
54
} , [ addSubscription , removeSubscription , feedKey ] ) ;
56
55
57
- return priceData . get ( feedKey ) ;
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
+ export const LivePrice = ( {
83
+ feedKey,
84
+ publisherKey,
85
+ } : {
86
+ feedKey : string ;
87
+ publisherKey ?: string | undefined ;
88
+ } ) =>
89
+ publisherKey ? (
90
+ < LiveComponentPrice feedKey = { feedKey } publisherKey = { publisherKey } />
91
+ ) : (
92
+ < LiveAggregatePrice feedKey = { feedKey } />
93
+ ) ;
94
+
95
+ const LiveAggregatePrice = ( { feedKey } : { feedKey : string } ) => {
96
+ const { prev, current } = useLivePrice ( feedKey ) ;
97
+ return (
98
+ < Price current = { current ?. aggregate . price } prev = { prev ?. aggregate . price } />
99
+ ) ;
100
+ } ;
101
+
102
+ const LiveComponentPrice = ( {
103
+ feedKey,
104
+ publisherKey,
105
+ } : {
106
+ feedKey : string ;
107
+ publisherKey : string ;
108
+ } ) => {
109
+ const { prev, current } = useLivePriceComponent ( feedKey , publisherKey ) ;
110
+ return < Price current = { current ?. latest . price } prev = { prev ?. latest . price } /> ;
58
111
} ;
59
112
60
- export const LivePrice = ( { feedKey } : { feedKey : string } ) => {
113
+ const Price = ( {
114
+ prev,
115
+ current,
116
+ } : {
117
+ prev ?: number | undefined ;
118
+ current ?: number | undefined ;
119
+ } ) => {
61
120
const numberFormatter = useNumberFormatter ( { maximumSignificantDigits : 5 } ) ;
62
- const price = useLivePrice ( feedKey ) ;
63
121
64
- return price === undefined ? (
122
+ return current === undefined ? (
65
123
< Skeleton width = { SKELETON_WIDTH } />
66
124
) : (
67
- < span className = { styles . price } data-direction = { price . direction } >
68
- { numberFormatter . format ( price . aggregate . price ) }
125
+ < span
126
+ className = { styles . price }
127
+ data-direction = { prev ? getChangeDirection ( prev , current ) : "flat" }
128
+ >
129
+ { numberFormatter . format ( current ) }
69
130
</ span >
70
131
) ;
71
132
} ;
72
133
73
- export const LiveConfidence = ( { feedKey } : { feedKey : string } ) => {
134
+ export const LiveConfidence = ( {
135
+ feedKey,
136
+ publisherKey,
137
+ } : {
138
+ feedKey : string ;
139
+ publisherKey ?: string | undefined ;
140
+ } ) =>
141
+ publisherKey === undefined ? (
142
+ < LiveAggregateConfidence feedKey = { feedKey } />
143
+ ) : (
144
+ < LiveComponentConfidence feedKey = { feedKey } publisherKey = { publisherKey } />
145
+ ) ;
146
+
147
+ const LiveAggregateConfidence = ( { feedKey } : { feedKey : string } ) => {
148
+ const { current } = useLivePrice ( feedKey ) ;
149
+ return < Confidence confidence = { current ?. aggregate . confidence } /> ;
150
+ } ;
151
+
152
+ const LiveComponentConfidence = ( {
153
+ feedKey,
154
+ publisherKey,
155
+ } : {
156
+ feedKey : string ;
157
+ publisherKey : string ;
158
+ } ) => {
159
+ const { current } = useLivePriceComponent ( feedKey , publisherKey ) ;
160
+ return < Confidence confidence = { current ?. latest . confidence } /> ;
161
+ } ;
162
+
163
+ const Confidence = ( { confidence } : { confidence ?: number | undefined } ) => {
74
164
const numberFormatter = useNumberFormatter ( { maximumSignificantDigits : 5 } ) ;
75
- const price = useLivePrice ( feedKey ) ;
76
165
77
166
return (
78
167
< span className = { styles . confidence } >
79
168
< PlusMinus className = { styles . plusMinus } />
80
- { price === undefined ? (
169
+ { confidence === undefined ? (
81
170
< Skeleton width = { SKELETON_WIDTH } />
82
171
) : (
83
- < span > { numberFormatter . format ( price . aggregate . confidence ) } </ span >
172
+ < span > { numberFormatter . format ( confidence ) } </ span >
84
173
) }
85
174
</ span >
86
175
) ;
87
176
} ;
88
177
89
178
export const LiveLastUpdated = ( { feedKey } : { feedKey : string } ) => {
90
- const price = useLivePrice ( feedKey ) ;
179
+ const { current } = useLivePrice ( feedKey ) ;
91
180
const formatterWithDate = useDateFormatter ( {
92
181
dateStyle : "short" ,
93
182
timeStyle : "medium" ,
@@ -96,15 +185,15 @@ export const LiveLastUpdated = ({ feedKey }: { feedKey: string }) => {
96
185
timeStyle : "medium" ,
97
186
} ) ;
98
187
const formattedTimestamp = useMemo ( ( ) => {
99
- if ( price ) {
100
- const timestamp = new Date ( Number ( price . timestamp * 1000n ) ) ;
188
+ if ( current ) {
189
+ const timestamp = new Date ( Number ( current . timestamp * 1000n ) ) ;
101
190
return isToday ( timestamp )
102
191
? formatterWithoutDate . format ( timestamp )
103
192
: formatterWithDate . format ( timestamp ) ;
104
193
} else {
105
194
return ;
106
195
}
107
- } , [ price , formatterWithDate , formatterWithoutDate ] ) ;
196
+ } , [ current , formatterWithDate , formatterWithoutDate ] ) ;
108
197
109
198
return formattedTimestamp ?? < Skeleton width = { SKELETON_WIDTH } /> ;
110
199
} ;
@@ -120,9 +209,27 @@ export const LiveValue = <T extends keyof PriceData>({
120
209
field,
121
210
defaultValue,
122
211
} : LiveValueProps < T > ) => {
123
- const price = useLivePrice ( feedKey ) ;
212
+ const { current } = useLivePrice ( feedKey ) ;
124
213
125
- return price ?. [ field ] ?. toString ( ) ?? defaultValue ;
214
+ return current ?. [ field ] ?. toString ( ) ?? defaultValue ;
215
+ } ;
216
+
217
+ type LiveComponentValueProps < T extends keyof PriceComponent [ "latest" ] > = {
218
+ field : T ;
219
+ feedKey : string ;
220
+ publisherKey : string ;
221
+ defaultValue ?: ReactNode | undefined ;
222
+ } ;
223
+
224
+ export const LiveComponentValue = < T extends keyof PriceComponent [ "latest" ] > ( {
225
+ feedKey,
226
+ field,
227
+ publisherKey,
228
+ defaultValue,
229
+ } : LiveComponentValueProps < T > ) => {
230
+ const { current } = useLivePriceComponent ( feedKey , publisherKey ) ;
231
+
232
+ return current ?. latest [ field ] . toString ( ) ?? defaultValue ;
126
233
} ;
127
234
128
235
const isToday = ( date : Date ) => {
@@ -137,7 +244,8 @@ const isToday = (date: Date) => {
137
244
const usePriceData = ( ) => {
138
245
const feedSubscriptions = useMap < string , number > ( [ ] ) ;
139
246
const [ feedKeys , setFeedKeys ] = useState < string [ ] > ( [ ] ) ;
140
- const priceData = useMap < string , Price > ( [ ] ) ;
247
+ const prevPriceData = useMap < string , PriceData > ( [ ] ) ;
248
+ const priceData = useMap < string , PriceData > ( [ ] ) ;
141
249
const logger = useLogger ( ) ;
142
250
143
251
useEffect ( ( ) => {
@@ -147,15 +255,15 @@ const usePriceData = () => {
147
255
// that symbol.
148
256
const uninitializedFeedKeys = feedKeys . filter ( ( key ) => ! priceData . has ( key ) ) ;
149
257
if ( uninitializedFeedKeys . length > 0 ) {
150
- client
151
- . getAssetPricesFromAccounts (
152
- uninitializedFeedKeys . map ( ( key ) => new PublicKey ( key ) ) ,
153
- )
258
+ getAssetPricesFromAccounts (
259
+ Cluster . Pythnet ,
260
+ uninitializedFeedKeys . map ( ( key ) => new PublicKey ( key ) ) ,
261
+ )
154
262
. then ( ( initialPrices ) => {
155
263
for ( const [ i , price ] of initialPrices . entries ( ) ) {
156
264
const key = uninitializedFeedKeys [ i ] ;
157
- if ( key ) {
158
- priceData . set ( key , { ... price , direction : "flat" } ) ;
265
+ if ( key && ! priceData . has ( key ) ) {
266
+ priceData . set ( key , price ) ;
159
267
}
160
268
}
161
269
} )
@@ -166,14 +274,15 @@ const usePriceData = () => {
166
274
167
275
// Then, we create a subscription to update prices live.
168
276
const connection = subscribe (
277
+ Cluster . Pythnet ,
169
278
feedKeys . map ( ( key ) => new PublicKey ( key ) ) ,
170
- ( { price_account } , price ) => {
279
+ ( { price_account } , data ) => {
171
280
if ( price_account ) {
172
- const prevPrice = priceData . get ( price_account ) ?. price ;
173
- priceData . set ( price_account , {
174
- ... price ,
175
- direction : getChangeDirection ( prevPrice , price . aggregate . price ) ,
176
- } ) ;
281
+ const prevData = priceData . get ( price_account ) ;
282
+ if ( prevData ) {
283
+ prevPriceData . set ( price_account , prevData ) ;
284
+ }
285
+ priceData . set ( price_account , data ) ;
177
286
}
178
287
} ,
179
288
) ;
@@ -186,7 +295,7 @@ const usePriceData = () => {
186
295
logger . error ( "Failed to unsubscribe from price updates" , error ) ;
187
296
} ) ;
188
297
} ;
189
- } , [ feedKeys , logger , priceData ] ) ;
298
+ } , [ feedKeys , logger , priceData , prevPriceData ] ) ;
190
299
191
300
const addSubscription = useCallback (
192
301
( key : string ) => {
@@ -214,6 +323,7 @@ const usePriceData = () => {
214
323
215
324
return {
216
325
priceData : new Map ( priceData ) ,
326
+ prevPriceData : new Map ( prevPriceData ) ,
217
327
addSubscription,
218
328
removeSubscription,
219
329
} ;
@@ -246,3 +356,5 @@ const getChangeDirection = (
246
356
return "down" ;
247
357
}
248
358
} ;
359
+
360
+ type ChangeDirection = "up" | "down" | "flat" ;
0 commit comments