1
+ import { AnchorProvider , Program , Wallet } from '@coral-xyz/anchor'
2
+ import {
3
+ getPythProgramKeyForCluster ,
4
+ pythOracleProgram ,
5
+ } from '@pythnetwork/client'
6
+ import { PythOracle } from '@pythnetwork/client/lib/anchor'
7
+ import { useAnchorWallet , useWallet } from '@solana/wallet-adapter-react'
8
+ import { TransactionInstruction } from '@solana/web3.js'
9
+ import {
10
+ createColumnHelper ,
11
+ flexRender ,
12
+ getCoreRowModel ,
13
+ useReactTable ,
14
+ } from '@tanstack/react-table'
15
+ import { useContext , useEffect , useState } from 'react'
16
+ import toast from 'react-hot-toast'
17
+ import { proposeInstructions } from 'xc-admin-common'
18
+ import { ClusterContext } from '../../contexts/ClusterContext'
1
19
import { usePythContext } from '../../contexts/PythContext'
20
+ import {
21
+ getMultisigCluster ,
22
+ SECURITY_MULTISIG ,
23
+ useMultisig ,
24
+ } from '../../hooks/useMultisig'
25
+ import { capitalizeFirstLetter } from '../../utils/capitalizeFirstLetter'
2
26
import ClusterSwitch from '../ClusterSwitch'
27
+ import Modal from '../common/Modal'
28
+ import EditButton from '../EditButton'
3
29
import Loadbar from '../loaders/Loadbar'
4
30
31
+ interface MinPublishersProps {
32
+ symbol : string
33
+ minPublishers : number
34
+ newMinPublishers ?: number
35
+ }
36
+
37
+ interface MinPublishersInfo {
38
+ prev : number
39
+ new : number
40
+ }
41
+
42
+ const columnHelper = createColumnHelper < MinPublishersProps > ( )
43
+
44
+ const defaultColumns = [
45
+ columnHelper . accessor ( 'symbol' , {
46
+ cell : ( info ) => info . getValue ( ) ,
47
+ header : ( ) => < span > Symbol</ span > ,
48
+ } ) ,
49
+ columnHelper . accessor ( 'minPublishers' , {
50
+ cell : ( props ) => {
51
+ const minPublishers = props . getValue ( )
52
+ return < span className = "mr-2" > { minPublishers } </ span >
53
+ } ,
54
+ header : ( ) => < span > Min Publishers</ span > ,
55
+ } ) ,
56
+ ]
57
+
5
58
const MinPublishers = ( ) => {
6
- const { rawConfig, dataIsLoading } = usePythContext ( )
59
+ const [ data , setData ] = useState < MinPublishersProps [ ] > ( [ ] )
60
+ const [ columns , setColumns ] = useState ( ( ) => [ ...defaultColumns ] )
61
+ const [ minPublishersChanges , setMinPublishersChanges ] =
62
+ useState < Record < string , MinPublishersInfo > > ( )
63
+ const [ editable , setEditable ] = useState ( false )
64
+ const [ isModalOpen , setIsModalOpen ] = useState ( false )
65
+ const [ isSendProposalButtonLoading , setIsSendProposalButtonLoading ] =
66
+ useState ( false )
67
+ const { cluster } = useContext ( ClusterContext )
68
+ const anchorWallet = useAnchorWallet ( )
69
+ const { isLoading : isMultisigLoading , squads } = useMultisig (
70
+ anchorWallet as Wallet
71
+ )
72
+ const { rawConfig, dataIsLoading, connection } = usePythContext ( )
73
+ const { connected } = useWallet ( )
74
+ const [ pythProgramClient , setPythProgramClient ] =
75
+ useState < Program < PythOracle > > ( )
76
+
77
+ const openModal = ( ) => {
78
+ setIsModalOpen ( true )
79
+ }
80
+
81
+ const closeModal = ( ) => {
82
+ setIsModalOpen ( false )
83
+ }
84
+
85
+ const handleEditButtonClick = ( ) => {
86
+ const nextState = ! editable
87
+ if ( nextState ) {
88
+ const newColumns = [
89
+ ...defaultColumns ,
90
+ columnHelper . accessor ( 'newMinPublishers' , {
91
+ cell : ( info ) => info . getValue ( ) ,
92
+ header : ( ) => < span > New Min Publishers</ span > ,
93
+ } ) ,
94
+ ]
95
+ setColumns ( newColumns )
96
+ } else {
97
+ if (
98
+ minPublishersChanges &&
99
+ Object . keys ( minPublishersChanges ) . length > 0
100
+ ) {
101
+ openModal ( )
102
+ setMinPublishersChanges ( minPublishersChanges )
103
+ } else {
104
+ setColumns ( defaultColumns )
105
+ }
106
+ }
107
+
108
+ setEditable ( nextState )
109
+ }
110
+
111
+ const handleEditMinPublishers = (
112
+ e : any ,
113
+ symbol : string ,
114
+ prevMinPublishers : number
115
+ ) => {
116
+ const newMinPublishers = Number ( e . target . textContent )
117
+ if ( prevMinPublishers !== newMinPublishers ) {
118
+ setMinPublishersChanges ( {
119
+ ...minPublishersChanges ,
120
+ [ symbol ] : {
121
+ prev : prevMinPublishers ,
122
+ new : newMinPublishers ,
123
+ } ,
124
+ } )
125
+ } else {
126
+ // delete symbol from minPublishersChanges if it exists
127
+ if ( minPublishersChanges && minPublishersChanges [ symbol ] ) {
128
+ delete minPublishersChanges [ symbol ]
129
+ }
130
+ setMinPublishersChanges ( minPublishersChanges )
131
+ }
132
+ }
133
+
134
+ useEffect ( ( ) => {
135
+ if ( ! dataIsLoading && rawConfig ) {
136
+ const minPublishersData : MinPublishersProps [ ] = [ ]
137
+ rawConfig . mappingAccounts
138
+ . sort (
139
+ ( mapping1 , mapping2 ) =>
140
+ mapping2 . products . length - mapping1 . products . length
141
+ ) [ 0 ]
142
+ . products . map ( ( product ) =>
143
+ product . priceAccounts . map ( ( priceAccount ) => {
144
+ minPublishersData . push ( {
145
+ symbol : product . metadata . symbol ,
146
+ minPublishers : priceAccount . minPub ,
147
+ } )
148
+ } )
149
+ )
150
+ setData ( minPublishersData )
151
+ }
152
+ } , [ setData , rawConfig , dataIsLoading ] )
153
+
154
+ const table = useReactTable ( {
155
+ data,
156
+ columns,
157
+ getCoreRowModel : getCoreRowModel ( ) ,
158
+ } )
159
+
160
+ const handleSendProposalButtonClick = async ( ) => {
161
+ if ( pythProgramClient && minPublishersChanges ) {
162
+ const instructions : TransactionInstruction [ ] = [ ]
163
+ Object . keys ( minPublishersChanges ) . forEach ( ( symbol ) => {
164
+ const { prev, new : newMinPublishers } = minPublishersChanges [ symbol ]
165
+ const priceAccountPubkey = rawConfig . mappingAccounts
166
+ . sort (
167
+ ( mapping1 , mapping2 ) =>
168
+ mapping2 . products . length - mapping1 . products . length
169
+ ) [ 0 ]
170
+ . products . find ( ( product ) => product . metadata . symbol === symbol ) !
171
+ . priceAccounts . find (
172
+ ( priceAccount ) => priceAccount . minPub === prev
173
+ ) ! . address
174
+
175
+ pythProgramClient . methods
176
+ . setMinPub ( newMinPublishers , [ 0 , 0 , 0 ] )
177
+ . accounts ( {
178
+ priceAccount : priceAccountPubkey ,
179
+ fundingAccount : squads ?. getAuthorityPDA (
180
+ SECURITY_MULTISIG [ getMultisigCluster ( cluster ) ] ,
181
+ 1
182
+ ) ,
183
+ } )
184
+ . instruction ( )
185
+ . then ( ( instruction ) => instructions . push ( instruction ) )
186
+ } )
187
+ if ( ! isMultisigLoading && squads ) {
188
+ setIsSendProposalButtonLoading ( true )
189
+ try {
190
+ const proposalPubkey = await proposeInstructions (
191
+ squads ,
192
+ SECURITY_MULTISIG [ getMultisigCluster ( cluster ) ] ,
193
+ instructions ,
194
+ false
195
+ )
196
+ toast . success ( `Proposal sent! 🚀 Proposal Pubkey: ${ proposalPubkey } ` )
197
+ setIsSendProposalButtonLoading ( false )
198
+ } catch ( e : any ) {
199
+ toast . error ( capitalizeFirstLetter ( e . message ) )
200
+ setIsSendProposalButtonLoading ( false )
201
+ }
202
+ }
203
+ }
204
+ }
205
+
206
+ // create anchor wallet when connected
207
+ useEffect ( ( ) => {
208
+ if ( connected ) {
209
+ const provider = new AnchorProvider (
210
+ connection ,
211
+ anchorWallet as Wallet ,
212
+ AnchorProvider . defaultOptions ( )
213
+ )
214
+ setPythProgramClient (
215
+ pythOracleProgram ( getPythProgramKeyForCluster ( cluster ) , provider )
216
+ )
217
+ }
218
+ } , [ anchorWallet , connection , connected , cluster ] )
7
219
8
220
return (
9
221
< div className = "relative" >
222
+ < Modal
223
+ isModalOpen = { isModalOpen }
224
+ setIsModalOpen = { setIsModalOpen }
225
+ closeModal = { closeModal }
226
+ changes = { minPublishersChanges }
227
+ handleSendProposalButtonClick = { handleSendProposalButtonClick }
228
+ isSendProposalButtonLoading = { isSendProposalButtonLoading }
229
+ />
10
230
< div className = "container flex flex-col items-center justify-between lg:flex-row" >
11
231
< div className = "mb-4 w-full text-left lg:mb-0" >
12
232
< h1 className = "h1 mb-4" > Min Publishers</ h1 >
13
233
</ div >
14
234
</ div >
15
235
< div className = "container" >
16
- < div className = "mb-4 md:mb-0" >
17
- < ClusterSwitch />
236
+ < div className = "flex justify-between" >
237
+ < div className = "mb-4 md:mb-0" >
238
+ < ClusterSwitch />
239
+ </ div >
240
+ < div className = "mb-4 md:mb-0" >
241
+ < EditButton editable = { editable } onClick = { handleEditButtonClick } />
242
+ </ div >
18
243
</ div >
19
244
< div className = "table-responsive relative mt-6" >
20
245
{ dataIsLoading ? (
@@ -23,50 +248,63 @@ const MinPublishers = () => {
23
248
</ div >
24
249
) : (
25
250
< div className = "table-responsive mb-10" >
26
- < table className = "w-full bg-darkGray text-left" >
251
+ < table className = "w-full table-auto bg-darkGray text-left" >
27
252
< thead >
28
- < tr >
29
- < th className = "base16 pt-8 pb-6 pl-4 pr-2 font-semibold opacity-60 lg:pl-14" >
30
- Symbol
31
- </ th >
32
- < th className = "base16 pt-8 pb-6 pl-1 pr-2 font-semibold opacity-60 lg:pl-14" >
33
- Minimum Publishers
34
- </ th >
35
- </ tr >
253
+ { table . getHeaderGroups ( ) . map ( ( headerGroup ) => (
254
+ < tr key = { headerGroup . id } >
255
+ { headerGroup . headers . map ( ( header ) => (
256
+ < th
257
+ key = { header . id }
258
+ className = {
259
+ header . column . id === 'symbol'
260
+ ? 'base16 pt-8 pb-6 pl-4 pr-2 font-semibold opacity-60 xl:pl-14'
261
+ : 'base16 pt-8 pb-6 pl-1 pr-2 font-semibold opacity-60'
262
+ }
263
+ >
264
+ { header . isPlaceholder
265
+ ? null
266
+ : flexRender (
267
+ header . column . columnDef . header ,
268
+ header . getContext ( )
269
+ ) }
270
+ </ th >
271
+ ) ) }
272
+ </ tr >
273
+ ) ) }
36
274
</ thead >
37
275
< tbody >
38
- { rawConfig . mappingAccounts . length ? (
39
- rawConfig . mappingAccounts
40
- . sort (
41
- ( mapping1 , mapping2 ) =>
42
- mapping2 . products . length - mapping1 . products . length
43
- ) [ 0 ]
44
- . products . map ( ( product ) =>
45
- product . priceAccounts . map ( ( priceAccount ) => {
46
- return (
47
- < tr
48
- key = { product . metadata . symbol }
49
- className = "border-t border-beige-300"
50
- >
51
- < td className = "py-3 pl-4 pr-2 lg:pl-14" >
52
- { product . metadata . symbol }
53
- </ td >
54
- < td className = "py-3 pl-1 lg:pl-14" >
55
- < span className = "mr-2" >
56
- { priceAccount . minPub }
57
- </ span >
58
- </ td >
59
- </ tr >
60
- )
61
- } )
62
- )
63
- ) : (
64
- < tr className = "border-t border-beige-300" >
65
- < td className = "py-3 pl-4 lg:pl-14" colSpan = { 2 } >
66
- No mapping accounts found.
67
- </ td >
276
+ { table . getRowModel ( ) . rows . map ( ( row ) => (
277
+ < tr key = { row . id } className = "border-t border-beige-300" >
278
+ { row . getVisibleCells ( ) . map ( ( cell ) => (
279
+ < td
280
+ key = { cell . id }
281
+ onBlur = { ( e ) =>
282
+ handleEditMinPublishers (
283
+ e ,
284
+ cell . row . original . symbol ,
285
+ cell . row . original . minPublishers
286
+ )
287
+ }
288
+ contentEditable = {
289
+ cell . column . id === 'newMinPublishers' && editable
290
+ ? true
291
+ : false
292
+ }
293
+ suppressContentEditableWarning = { true }
294
+ className = {
295
+ cell . column . id === 'symbol'
296
+ ? 'py-3 pl-4 pr-2 xl:pl-14'
297
+ : 'items-center py-3 pl-1 pr-4'
298
+ }
299
+ >
300
+ { flexRender (
301
+ cell . column . columnDef . cell ,
302
+ cell . getContext ( )
303
+ ) }
304
+ </ td >
305
+ ) ) }
68
306
</ tr >
69
- ) }
307
+ ) ) }
70
308
</ tbody >
71
309
</ table >
72
310
</ div >
0 commit comments