1
- import { useState } from "react" ;
1
+ import React from "react" ;
2
2
import CopyIcon from "./icons/CopyIcon" ;
3
3
import { mapValues } from "../utils/ObjectHelpers" ;
4
+ import { useCopyToClipboard } from "../utils/useCopyToClipboard" ;
4
5
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
11
7
interface 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 ;
15
13
}
16
14
17
15
interface SponsoredFeedsTableProps {
18
16
feeds : SponsoredFeed [ ] ;
19
17
networkName : string ;
20
18
}
21
19
20
+ interface UpdateParamsProps {
21
+ feed : SponsoredFeed ;
22
+ isDefault : boolean ;
23
+ }
24
+
22
25
/**
23
26
* Helper functions
24
27
*/
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
+ } ;
25
54
26
55
// 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` ;
29
62
} ;
30
63
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
+ } ;
52
90
53
91
export const SponsoredFeedsTable = ( {
54
92
feeds,
55
93
networkName,
56
94
} : SponsoredFeedsTableProps ) => {
57
- const [ copiedId , setCopiedId ] = useState < string | null > ( null ) ;
95
+ const { copiedText , copyToClipboard } = useCopyToClipboard ( ) ;
58
96
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
+ }
65
108
66
109
// Calculate parameter statistics
67
110
const paramCounts = mapValues (
68
- Object . groupBy ( feeds , ( feed ) => formatUpdateParams ( feed . updateParameters ) ) ,
111
+ Object . groupBy ( feeds , ( feed ) => formatUpdateParams ( feed ) ) ,
69
112
( feeds : SponsoredFeed [ ] ) => feeds . length
70
113
) ;
71
114
72
- const defaultParams = Object . entries ( paramCounts ) . sort (
115
+ const paramEntries = Object . entries ( paramCounts ) . sort (
73
116
( [ , a ] , [ , b ] ) => b - a
74
- ) [ 0 ] [ 0 ] ;
117
+ ) ;
118
+ const defaultParams = paramEntries . length > 0 ? paramEntries [ 0 ] [ 0 ] : "" ;
75
119
76
120
return (
77
121
< div className = "my-6" >
@@ -123,33 +167,31 @@ export const SponsoredFeedsTable = ({
123
167
</ tr >
124
168
</ thead >
125
169
< 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 ) ;
130
172
const isDefault = formattedParams === defaultParams ;
131
173
132
174
return (
133
175
< tr
134
- key = { feed . priceFeedId }
176
+ key = { feed . id }
135
177
className = "border-b border-gray-100 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800/30"
136
178
>
137
179
< td className = "px-3 py-2 align-top" >
138
180
< span className = "font-medium text-gray-900 dark:text-gray-100" >
139
- { feed . name }
181
+ { feed . alias }
140
182
</ span >
141
183
</ td >
142
184
< td className = "px-3 py-2 align-top" >
143
185
< div className = "flex items-start gap-2" >
144
186
< 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 }
146
188
</ code >
147
189
< button
148
- onClick = { ( ) => copyToClipboard ( feed . priceFeedId ) }
190
+ onClick = { ( ) => copyToClipboard ( feed . id ) }
149
191
className = "p-1 hover:bg-gray-200 dark:hover:bg-gray-600 rounded flex-shrink-0 mt-0.5"
150
192
title = "Copy Price Feed ID"
151
193
>
152
- { copiedId === feed . priceFeedId ? (
194
+ { copiedText === feed . id ? (
153
195
< span className = "text-green-500 text-xs font-bold" >
154
196
✓
155
197
</ span >
@@ -160,7 +202,7 @@ export const SponsoredFeedsTable = ({
160
202
</ div >
161
203
</ td >
162
204
< td className = "px-3 py-2 align-top" >
163
- { renderUpdateParams ( feed . updateParameters , isDefault ) }
205
+ < UpdateParams feed = { feed } isDefault = { isDefault } />
164
206
</ td >
165
207
</ tr >
166
208
) ;
0 commit comments