1- import { useState } from "react" ;
1+ import React from "react" ;
22import CopyIcon from "./icons/CopyIcon" ;
33import { mapValues } from "../utils/ObjectHelpers" ;
4+ import { useCopyToClipboard } from "../utils/useCopyToClipboard" ;
45
5- interface UpdateParameters {
6- heartbeatLength : number ;
7- heartbeatUnit : "second" | "minute" | "hour" ;
8- priceDeviation : number ;
9- }
10-
6+ // SponsoredFeed interface has the same structure as defined in deployment yaml/json files
117interface SponsoredFeed {
12- name : string ;
13- priceFeedId : string ;
14- updateParameters : UpdateParameters ;
8+ alias : string ; // name of the feed
9+ id : string ; // price feed id
10+ time_difference : number ; // in seconds
11+ price_deviation : number ;
12+ confidence_ratio : number ;
1513}
1614
1715interface SponsoredFeedsTableProps {
1816 feeds : SponsoredFeed [ ] ;
1917 networkName : string ;
2018}
2119
20+ interface UpdateParamsProps {
21+ feed : SponsoredFeed ;
22+ isDefault : boolean ;
23+ }
24+
2225/**
2326 * Helper functions
2427 */
28+ // Convert time_difference (seconds) to human readable format
29+ const formatTimeUnit = ( seconds : number ) : { value : number ; unit : string } => {
30+ // @ts -expect-error - Intl.DurationFormat is not a standard type
31+ const duration = new Intl . DurationFormat ( "en" , {
32+ style : "long" ,
33+ numeric : "auto" ,
34+ } ) ;
35+ let durationObj : { hours ?: number ; minutes ?: number ; seconds ?: number } ;
36+
37+ if ( seconds >= 3600 ) {
38+ durationObj = { hours : Math . floor ( seconds / 3600 ) } ;
39+ } else if ( seconds >= 60 ) {
40+ durationObj = { minutes : Math . floor ( seconds / 60 ) } ;
41+ } else {
42+ durationObj = { seconds } ;
43+ }
44+
45+ const parts = duration . formatToParts ( durationObj ) ;
46+ const intPart = parts . find ( ( p : any ) => p . type === "integer" ) ;
47+ if ( intPart ) {
48+ return { value : Number ( intPart . value ) , unit : intPart . unit } ;
49+ } else {
50+ // fallback in case formatting fails
51+ return { value : seconds , unit : "second" } ;
52+ }
53+ } ;
2554
2655// Format update parameters as a string for grouping
27- const formatUpdateParams = ( params : UpdateParameters ) : string => {
28- return `${ params . heartbeatLength } ${ params . heartbeatUnit } heartbeat / ${ params . priceDeviation } % price deviation` ;
56+ const formatUpdateParams = ( feed : SponsoredFeed ) : string => {
57+ const timeFormat = formatTimeUnit ( feed . time_difference ) ;
58+ const timeStr = `${ timeFormat . value } ${ timeFormat . unit } ${
59+ timeFormat . value !== 1 ? "s" : ""
60+ } `;
61+ return `${ timeStr } heartbeat / ${ feed . price_deviation } % price deviation` ;
2962} ;
3063
31- // Render update parameters with proper styling
32- const renderUpdateParams = ( params : UpdateParameters , isDefault : boolean ) => (
33- < div className = "flex items-start gap-1.5" >
34- < div
35- className = { `w-1.5 h-1.5 rounded-full mt-1 flex-shrink-0 ${
36- isDefault ? "bg-green-500" : "bg-orange-500"
37- } `}
38- > </ div >
39- < span
40- className = { `text-xs leading-relaxed font-medium ${
41- isDefault
42- ? "text-gray-700 dark:text-gray-300"
43- : "text-orange-600 dark:text-orange-400"
44- } `}
45- >
46- < strong > { params . heartbeatLength } </ strong > { params . heartbeatUnit } heartbeat
47- < br />
48- < strong > { params . priceDeviation } %</ strong > price deviation
49- </ span >
50- </ div >
51- ) ;
64+ const UpdateParams = ( { feed, isDefault } : UpdateParamsProps ) => {
65+ const timeFormat = formatTimeUnit ( feed . time_difference ) ;
66+ const timeStr =
67+ timeFormat . value === 1 ? timeFormat . unit : `${ timeFormat . unit } s` ;
68+
69+ return (
70+ < div className = "flex items-start gap-1.5" >
71+ < div
72+ className = { `w-1.5 h-1.5 rounded-full mt-1 flex-shrink-0 ${
73+ isDefault ? "bg-green-500" : "bg-orange-500"
74+ } `}
75+ > </ div >
76+ < span
77+ className = { `text-xs leading-relaxed font-medium ${
78+ isDefault
79+ ? "text-gray-700 dark:text-gray-300"
80+ : "text-orange-600 dark:text-orange-400"
81+ } `}
82+ >
83+ < strong > { timeFormat . value } </ strong > { timeStr } heartbeat
84+ < br />
85+ < strong > { feed . price_deviation } %</ strong > price deviation
86+ </ span >
87+ </ div >
88+ ) ;
89+ } ;
5290
5391export const SponsoredFeedsTable = ( {
5492 feeds,
5593 networkName,
5694} : SponsoredFeedsTableProps ) => {
57- const [ copiedId , setCopiedId ] = useState < string | null > ( null ) ;
95+ const { copiedText , copyToClipboard } = useCopyToClipboard ( ) ;
5896
59- const copyToClipboard = ( text : string ) => {
60- navigator . clipboard . writeText ( text ) . then ( ( ) => {
61- setCopiedId ( text ) ;
62- setTimeout ( ( ) => setCopiedId ( null ) , 2000 ) ;
63- } ) ;
64- } ;
97+ // Handle empty feeds
98+ if ( feeds . length === 0 ) {
99+ return (
100+ < div className = "my-6" >
101+ < p className = "mb-3" >
102+ No sponsored price feeds are currently available for{ " " }
103+ < strong > { networkName } </ strong > .
104+ </ p >
105+ </ div >
106+ ) ;
107+ }
65108
66109 // Calculate parameter statistics
67110 const paramCounts = mapValues (
68- Object . groupBy ( feeds , ( feed ) => formatUpdateParams ( feed . updateParameters ) ) ,
111+ Object . groupBy ( feeds , ( feed ) => formatUpdateParams ( feed ) ) ,
69112 ( feeds : SponsoredFeed [ ] ) => feeds . length
70113 ) ;
71114
72- const defaultParams = Object . entries ( paramCounts ) . sort (
115+ const paramEntries = Object . entries ( paramCounts ) . sort (
73116 ( [ , a ] , [ , b ] ) => b - a
74- ) [ 0 ] [ 0 ] ;
117+ ) ;
118+ const defaultParams = paramEntries . length > 0 ? paramEntries [ 0 ] [ 0 ] : "" ;
75119
76120 return (
77121 < div className = "my-6" >
@@ -123,33 +167,31 @@ export const SponsoredFeedsTable = ({
123167 </ tr >
124168 </ thead >
125169 < tbody className = "bg-white dark:bg-gray-900" >
126- { feeds . map ( ( feed , index ) => {
127- const formattedParams = formatUpdateParams (
128- feed . updateParameters
129- ) ;
170+ { feeds . map ( ( feed ) => {
171+ const formattedParams = formatUpdateParams ( feed ) ;
130172 const isDefault = formattedParams === defaultParams ;
131173
132174 return (
133175 < tr
134- key = { feed . priceFeedId }
176+ key = { feed . id }
135177 className = "border-b border-gray-100 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800/30"
136178 >
137179 < td className = "px-3 py-2 align-top" >
138180 < span className = "font-medium text-gray-900 dark:text-gray-100" >
139- { feed . name }
181+ { feed . alias }
140182 </ span >
141183 </ td >
142184 < td className = "px-3 py-2 align-top" >
143185 < div className = "flex items-start gap-2" >
144186 < code className = "text-xs font-mono text-gray-600 dark:text-gray-400 flex-1 break-all leading-relaxed" >
145- { feed . priceFeedId }
187+ { feed . id }
146188 </ code >
147189 < button
148- onClick = { ( ) => copyToClipboard ( feed . priceFeedId ) }
190+ onClick = { ( ) => copyToClipboard ( feed . id ) }
149191 className = "p-1 hover:bg-gray-200 dark:hover:bg-gray-600 rounded flex-shrink-0 mt-0.5"
150192 title = "Copy Price Feed ID"
151193 >
152- { copiedId === feed . priceFeedId ? (
194+ { copiedText === feed . id ? (
153195 < span className = "text-green-500 text-xs font-bold" >
154196 ✓
155197 </ span >
@@ -160,7 +202,7 @@ export const SponsoredFeedsTable = ({
160202 </ div >
161203 </ td >
162204 < td className = "px-3 py-2 align-top" >
163- { renderUpdateParams ( feed . updateParameters , isDefault ) }
205+ < UpdateParams feed = { feed } isDefault = { isDefault } />
164206 </ td >
165207 </ tr >
166208 ) ;
0 commit comments