22
33import { PlusMinus } from "@phosphor-icons/react/dist/ssr/PlusMinus" ;
44import { useLogger } from "@pythnetwork/app-logger" ;
5- import type { PriceData } from "@pythnetwork/client" ;
5+ import type { PriceData , PriceComponent } from "@pythnetwork/client" ;
66import { Skeleton } from "@pythnetwork/component-library/Skeleton" ;
77import { useMap } from "@react-hookz/web" ;
88import { PublicKey } from "@solana/web3.js" ;
@@ -19,20 +19,18 @@ import {
1919import { useNumberFormatter , useDateFormatter } from "react-aria" ;
2020
2121import styles from "./index.module.scss" ;
22- import { client , subscribe } from "../../services/pyth" ;
22+ import {
23+ Cluster ,
24+ subscribe ,
25+ getAssetPricesFromAccounts ,
26+ } from "../../services/pyth" ;
2327
2428export const SKELETON_WIDTH = 20 ;
2529
2630const LivePricesContext = createContext <
2731 ReturnType < typeof usePriceData > | undefined
2832> ( undefined ) ;
2933
30- type Price = PriceData & {
31- direction : ChangeDirection ;
32- } ;
33-
34- type ChangeDirection = "up" | "down" | "flat" ;
35-
3634type LivePricesProviderProps = Omit <
3735 ComponentProps < typeof LivePricesContext > ,
3836 "value"
@@ -45,7 +43,8 @@ export const LivePricesProvider = (props: LivePricesProviderProps) => {
4543} ;
4644
4745export const useLivePrice = ( feedKey : string ) => {
48- const { priceData, addSubscription, removeSubscription } = useLivePrices ( ) ;
46+ const { priceData, prevPriceData, addSubscription, removeSubscription } =
47+ useLivePrices ( ) ;
4948
5049 useEffect ( ( ) => {
5150 addSubscription ( feedKey ) ;
@@ -54,40 +53,130 @@ export const useLivePrice = (feedKey: string) => {
5453 } ;
5554 } , [ addSubscription , removeSubscription , feedKey ] ) ;
5655
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 } /> ;
58111} ;
59112
60- export const LivePrice = ( { feedKey } : { feedKey : string } ) => {
113+ const Price = ( {
114+ prev,
115+ current,
116+ } : {
117+ prev ?: number | undefined ;
118+ current ?: number | undefined ;
119+ } ) => {
61120 const numberFormatter = useNumberFormatter ( { maximumSignificantDigits : 5 } ) ;
62- const price = useLivePrice ( feedKey ) ;
63121
64- return price === undefined ? (
122+ return current === undefined ? (
65123 < Skeleton width = { SKELETON_WIDTH } />
66124 ) : (
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 ) }
69130 </ span >
70131 ) ;
71132} ;
72133
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 } ) => {
74164 const numberFormatter = useNumberFormatter ( { maximumSignificantDigits : 5 } ) ;
75- const price = useLivePrice ( feedKey ) ;
76165
77166 return (
78167 < span className = { styles . confidence } >
79168 < PlusMinus className = { styles . plusMinus } />
80- { price === undefined ? (
169+ { confidence === undefined ? (
81170 < Skeleton width = { SKELETON_WIDTH } />
82171 ) : (
83- < span > { numberFormatter . format ( price . aggregate . confidence ) } </ span >
172+ < span > { numberFormatter . format ( confidence ) } </ span >
84173 ) }
85174 </ span >
86175 ) ;
87176} ;
88177
89178export const LiveLastUpdated = ( { feedKey } : { feedKey : string } ) => {
90- const price = useLivePrice ( feedKey ) ;
179+ const { current } = useLivePrice ( feedKey ) ;
91180 const formatterWithDate = useDateFormatter ( {
92181 dateStyle : "short" ,
93182 timeStyle : "medium" ,
@@ -96,15 +185,15 @@ export const LiveLastUpdated = ({ feedKey }: { feedKey: string }) => {
96185 timeStyle : "medium" ,
97186 } ) ;
98187 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 ) ) ;
101190 return isToday ( timestamp )
102191 ? formatterWithoutDate . format ( timestamp )
103192 : formatterWithDate . format ( timestamp ) ;
104193 } else {
105194 return ;
106195 }
107- } , [ price , formatterWithDate , formatterWithoutDate ] ) ;
196+ } , [ current , formatterWithDate , formatterWithoutDate ] ) ;
108197
109198 return formattedTimestamp ?? < Skeleton width = { SKELETON_WIDTH } /> ;
110199} ;
@@ -120,9 +209,27 @@ export const LiveValue = <T extends keyof PriceData>({
120209 field,
121210 defaultValue,
122211} : LiveValueProps < T > ) => {
123- const price = useLivePrice ( feedKey ) ;
212+ const { current } = useLivePrice ( feedKey ) ;
124213
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 ;
126233} ;
127234
128235const isToday = ( date : Date ) => {
@@ -137,7 +244,8 @@ const isToday = (date: Date) => {
137244const usePriceData = ( ) => {
138245 const feedSubscriptions = useMap < string , number > ( [ ] ) ;
139246 const [ feedKeys , setFeedKeys ] = useState < string [ ] > ( [ ] ) ;
140- const priceData = useMap < string , Price > ( [ ] ) ;
247+ const prevPriceData = useMap < string , PriceData > ( [ ] ) ;
248+ const priceData = useMap < string , PriceData > ( [ ] ) ;
141249 const logger = useLogger ( ) ;
142250
143251 useEffect ( ( ) => {
@@ -147,15 +255,15 @@ const usePriceData = () => {
147255 // that symbol.
148256 const uninitializedFeedKeys = feedKeys . filter ( ( key ) => ! priceData . has ( key ) ) ;
149257 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+ )
154262 . then ( ( initialPrices ) => {
155263 for ( const [ i , price ] of initialPrices . entries ( ) ) {
156264 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 ) ;
159267 }
160268 }
161269 } )
@@ -166,14 +274,15 @@ const usePriceData = () => {
166274
167275 // Then, we create a subscription to update prices live.
168276 const connection = subscribe (
277+ Cluster . Pythnet ,
169278 feedKeys . map ( ( key ) => new PublicKey ( key ) ) ,
170- ( { price_account } , price ) => {
279+ ( { price_account } , data ) => {
171280 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 ) ;
177286 }
178287 } ,
179288 ) ;
@@ -186,7 +295,7 @@ const usePriceData = () => {
186295 logger . error ( "Failed to unsubscribe from price updates" , error ) ;
187296 } ) ;
188297 } ;
189- } , [ feedKeys , logger , priceData ] ) ;
298+ } , [ feedKeys , logger , priceData , prevPriceData ] ) ;
190299
191300 const addSubscription = useCallback (
192301 ( key : string ) => {
@@ -214,6 +323,7 @@ const usePriceData = () => {
214323
215324 return {
216325 priceData : new Map ( priceData ) ,
326+ prevPriceData : new Map ( prevPriceData ) ,
217327 addSubscription,
218328 removeSubscription,
219329 } ;
@@ -246,3 +356,5 @@ const getChangeDirection = (
246356 return "down" ;
247357 }
248358} ;
359+
360+ type ChangeDirection = "up" | "down" | "flat" ;
0 commit comments