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'
119import { usePythContext } from '../../contexts/PythContext'
20+ import {
21+ getMultisigCluster ,
22+ SECURITY_MULTISIG ,
23+ useMultisig ,
24+ } from '../../hooks/useMultisig'
25+ import { capitalizeFirstLetter } from '../../utils/capitalizeFirstLetter'
226import ClusterSwitch from '../ClusterSwitch'
27+ import Modal from '../common/Modal'
28+ import EditButton from '../EditButton'
329import Loadbar from '../loaders/Loadbar'
430
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+
558const 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 ] )
7219
8220 return (
9221 < div className = "relative" >
222+ < Modal
223+ isModalOpen = { isModalOpen }
224+ setIsModalOpen = { setIsModalOpen }
225+ closeModal = { closeModal }
226+ changes = { minPublishersChanges }
227+ handleSendProposalButtonClick = { handleSendProposalButtonClick }
228+ isSendProposalButtonLoading = { isSendProposalButtonLoading }
229+ />
10230 < div className = "container flex flex-col items-center justify-between lg:flex-row" >
11231 < div className = "mb-4 w-full text-left lg:mb-0" >
12232 < h1 className = "h1 mb-4" > Min Publishers</ h1 >
13233 </ div >
14234 </ div >
15235 < 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 >
18243 </ div >
19244 < div className = "table-responsive relative mt-6" >
20245 { dataIsLoading ? (
@@ -23,50 +248,63 @@ const MinPublishers = () => {
23248 </ div >
24249 ) : (
25250 < 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" >
27252 < 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+ ) ) }
36274 </ thead >
37275 < 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+ ) ) }
68306 </ tr >
69- ) }
307+ ) ) }
70308 </ tbody >
71309 </ table >
72310 </ div >
0 commit comments