Skip to content

Commit 2c6eb7d

Browse files
authored
[xc-admin] min pub page (#519)
1 parent 227079b commit 2c6eb7d

File tree

4 files changed

+303
-65
lines changed

4 files changed

+303
-65
lines changed

governance/xc-admin/packages/xc-admin-frontend/components/tabs/MinPublishers.tsx

Lines changed: 281 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,245 @@
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'
119
import { usePythContext } from '../../contexts/PythContext'
20+
import {
21+
getMultisigCluster,
22+
SECURITY_MULTISIG,
23+
useMultisig,
24+
} from '../../hooks/useMultisig'
25+
import { capitalizeFirstLetter } from '../../utils/capitalizeFirstLetter'
226
import ClusterSwitch from '../ClusterSwitch'
27+
import Modal from '../common/Modal'
28+
import EditButton from '../EditButton'
329
import 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+
558
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])
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

Comments
 (0)