UpdatePermissions.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  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 { PublicKey } from '@solana/web3.js'
  9. import {
  10. createColumnHelper,
  11. flexRender,
  12. getCoreRowModel,
  13. useReactTable,
  14. } from '@tanstack/react-table'
  15. import copy from 'copy-to-clipboard'
  16. import { useContext, useEffect, useState } from 'react'
  17. import toast from 'react-hot-toast'
  18. import { proposeInstructions } from 'xc-admin-common'
  19. import { ClusterContext } from '../../contexts/ClusterContext'
  20. import { usePythContext } from '../../contexts/PythContext'
  21. import {
  22. getMultisigCluster,
  23. UPGRADE_MULTISIG,
  24. useMultisig,
  25. } from '../../hooks/useMultisig'
  26. import CopyIcon from '../../images/icons/copy.inline.svg'
  27. import { capitalizeFirstLetter } from '../../utils/capitalizeFirstLetter'
  28. import ClusterSwitch from '../ClusterSwitch'
  29. import Modal from '../common/Modal'
  30. import EditButton from '../EditButton'
  31. import Loadbar from '../loaders/Loadbar'
  32. interface UpdatePermissionsProps {
  33. account: PermissionAccount
  34. pubkey: string
  35. newPubkey?: string
  36. }
  37. const DEFAULT_DATA: UpdatePermissionsProps[] = [
  38. {
  39. account: 'Master Authority',
  40. pubkey: new PublicKey(0).toBase58(),
  41. },
  42. {
  43. account: 'Data Curation Authority',
  44. pubkey: new PublicKey(0).toBase58(),
  45. },
  46. {
  47. account: 'Security Authority',
  48. pubkey: new PublicKey(0).toBase58(),
  49. },
  50. ]
  51. const BPF_UPGRADABLE_LOADER = new PublicKey(
  52. 'BPFLoaderUpgradeab1e11111111111111111111111'
  53. )
  54. const columnHelper = createColumnHelper<UpdatePermissionsProps>()
  55. const defaultColumns = [
  56. columnHelper.accessor('account', {
  57. cell: (info) => info.getValue(),
  58. header: () => <span>Account</span>,
  59. }),
  60. columnHelper.accessor('pubkey', {
  61. cell: (props) => {
  62. const pubkey = props.getValue()
  63. return (
  64. <>
  65. <div
  66. className="-ml-1 inline-flex cursor-pointer items-center px-1 hover:bg-dark hover:text-white active:bg-darkGray3"
  67. onClick={() => {
  68. copy(pubkey)
  69. }}
  70. >
  71. <span className="mr-2 hidden lg:block">{pubkey}</span>
  72. <span className="mr-2 lg:hidden">
  73. {pubkey.slice(0, 6) + '...' + pubkey.slice(-6)}
  74. </span>{' '}
  75. <CopyIcon className="shrink-0" />
  76. </div>
  77. </>
  78. )
  79. },
  80. header: () => <span>Public Key</span>,
  81. }),
  82. ]
  83. type PermissionAccount =
  84. | 'Master Authority'
  85. | 'Data Curation Authority'
  86. | 'Security Authority'
  87. interface PermissionAccountInfo {
  88. prev: string
  89. new: string
  90. }
  91. const UpdatePermissions = () => {
  92. const [data, setData] = useState(() => [...DEFAULT_DATA])
  93. const [columns, setColumns] = useState(() => [...defaultColumns])
  94. const [pubkeyChanges, setPubkeyChanges] =
  95. useState<Partial<Record<PermissionAccount, PermissionAccountInfo>>>()
  96. const [finalPubkeyChanges, setFinalPubkeyChanges] =
  97. useState<Record<PermissionAccount, PermissionAccountInfo>>()
  98. const [editable, setEditable] = useState(false)
  99. const [isModalOpen, setIsModalOpen] = useState(false)
  100. const [isSendProposalButtonLoading, setIsSendProposalButtonLoading] =
  101. useState(false)
  102. const { cluster } = useContext(ClusterContext)
  103. const anchorWallet = useAnchorWallet()
  104. const { isLoading: isMultisigLoading, squads } = useMultisig(
  105. anchorWallet as Wallet
  106. )
  107. const { rawConfig, dataIsLoading, connection } = usePythContext()
  108. const { connected } = useWallet()
  109. const [pythProgramClient, setPythProgramClient] =
  110. useState<Program<PythOracle>>()
  111. useEffect(() => {
  112. if (rawConfig.permissionAccount) {
  113. const masterAuthority =
  114. rawConfig.permissionAccount.masterAuthority.toBase58()
  115. const dataCurationAuthority =
  116. rawConfig.permissionAccount.dataCurationAuthority.toBase58()
  117. const securityAuthority =
  118. rawConfig.permissionAccount.securityAuthority.toBase58()
  119. setData([
  120. {
  121. account: 'Master Authority',
  122. pubkey: masterAuthority,
  123. },
  124. {
  125. account: 'Data Curation Authority',
  126. pubkey: dataCurationAuthority,
  127. },
  128. {
  129. account: 'Security Authority',
  130. pubkey: securityAuthority,
  131. },
  132. ])
  133. } else {
  134. setData([...DEFAULT_DATA])
  135. }
  136. }, [rawConfig])
  137. const table = useReactTable({
  138. data,
  139. columns,
  140. getCoreRowModel: getCoreRowModel(),
  141. })
  142. const backfillPubkeyChanges = () => {
  143. const newPubkeyChanges: Record<PermissionAccount, PermissionAccountInfo> = {
  144. 'Master Authority': {
  145. prev: data[0].pubkey,
  146. new: data[0].pubkey,
  147. },
  148. 'Data Curation Authority': {
  149. prev: data[1].pubkey,
  150. new: data[1].pubkey,
  151. },
  152. 'Security Authority': {
  153. prev: data[2].pubkey,
  154. new: data[2].pubkey,
  155. },
  156. }
  157. if (pubkeyChanges) {
  158. Object.keys(pubkeyChanges).forEach((key) => {
  159. newPubkeyChanges[key as PermissionAccount] = pubkeyChanges[
  160. key as PermissionAccount
  161. ] as PermissionAccountInfo
  162. })
  163. }
  164. return newPubkeyChanges
  165. }
  166. const handleEditButtonClick = () => {
  167. const nextState = !editable
  168. if (nextState) {
  169. const newColumns = [
  170. ...defaultColumns,
  171. columnHelper.accessor('newPubkey', {
  172. cell: (info) => info.getValue(),
  173. header: () => <span>New Public Key</span>,
  174. }),
  175. ]
  176. setColumns(newColumns)
  177. } else {
  178. if (pubkeyChanges && Object.keys(pubkeyChanges).length > 0) {
  179. openModal()
  180. setFinalPubkeyChanges(backfillPubkeyChanges())
  181. } else {
  182. setColumns(defaultColumns)
  183. }
  184. }
  185. setEditable(nextState)
  186. }
  187. const openModal = () => {
  188. setIsModalOpen(true)
  189. }
  190. const closeModal = () => {
  191. setIsModalOpen(false)
  192. }
  193. // check if pubkey is valid
  194. const isValidPubkey = (pubkey: string) => {
  195. try {
  196. new PublicKey(pubkey)
  197. return true
  198. } catch (e) {
  199. return false
  200. }
  201. }
  202. const handleEditPubkey = (
  203. e: any,
  204. account: PermissionAccount,
  205. prevPubkey: string
  206. ) => {
  207. const newPubkey = e.target.textContent
  208. if (isValidPubkey(newPubkey) && newPubkey !== prevPubkey) {
  209. setPubkeyChanges({
  210. ...pubkeyChanges,
  211. [account]: {
  212. prev: prevPubkey,
  213. new: newPubkey,
  214. },
  215. })
  216. } else {
  217. // delete account from pubkeyChanges if it exists
  218. if (pubkeyChanges && pubkeyChanges[account]) {
  219. delete pubkeyChanges[account]
  220. }
  221. setPubkeyChanges(pubkeyChanges)
  222. }
  223. }
  224. const handleSendProposalButtonClick = () => {
  225. if (pythProgramClient && finalPubkeyChanges) {
  226. const programDataAccount = PublicKey.findProgramAddressSync(
  227. [pythProgramClient?.programId.toBuffer()],
  228. BPF_UPGRADABLE_LOADER
  229. )[0]
  230. pythProgramClient?.methods
  231. .updPermissions(
  232. new PublicKey(finalPubkeyChanges['Master Authority'].new),
  233. new PublicKey(finalPubkeyChanges['Data Curation Authority'].new),
  234. new PublicKey(finalPubkeyChanges['Security Authority'].new)
  235. )
  236. .accounts({
  237. upgradeAuthority: squads?.getAuthorityPDA(
  238. UPGRADE_MULTISIG[getMultisigCluster(cluster)],
  239. 1
  240. ),
  241. programDataAccount,
  242. })
  243. .instruction()
  244. .then(async (instruction) => {
  245. if (!isMultisigLoading && squads) {
  246. setIsSendProposalButtonLoading(true)
  247. try {
  248. const proposalPubkey = await proposeInstructions(
  249. squads,
  250. UPGRADE_MULTISIG[getMultisigCluster(cluster)],
  251. [instruction],
  252. false
  253. )
  254. toast.success(
  255. `Proposal sent! 🚀 Proposal Pubkey: ${proposalPubkey}`
  256. )
  257. setIsSendProposalButtonLoading(false)
  258. } catch (e: any) {
  259. toast.error(capitalizeFirstLetter(e.message))
  260. setIsSendProposalButtonLoading(false)
  261. }
  262. }
  263. })
  264. }
  265. }
  266. // create anchor wallet when connected
  267. useEffect(() => {
  268. if (connected) {
  269. const provider = new AnchorProvider(
  270. connection,
  271. anchorWallet as Wallet,
  272. AnchorProvider.defaultOptions()
  273. )
  274. setPythProgramClient(
  275. pythOracleProgram(getPythProgramKeyForCluster(cluster), provider)
  276. )
  277. }
  278. }, [anchorWallet, connection, connected, cluster])
  279. return (
  280. <div className="relative">
  281. <Modal
  282. isModalOpen={isModalOpen}
  283. setIsModalOpen={setIsModalOpen}
  284. closeModal={closeModal}
  285. changes={pubkeyChanges}
  286. handleSendProposalButtonClick={handleSendProposalButtonClick}
  287. isSendProposalButtonLoading={isSendProposalButtonLoading}
  288. />
  289. <div className="container flex flex-col items-center justify-between lg:flex-row">
  290. <div className="mb-4 w-full text-left lg:mb-0">
  291. <h1 className="h1 mb-4">Update Permissions</h1>
  292. </div>
  293. </div>
  294. <div className="container">
  295. <div className="flex justify-between">
  296. <div className="mb-4 md:mb-0">
  297. <ClusterSwitch />
  298. </div>
  299. <div className="mb-4 md:mb-0">
  300. <EditButton editable={editable} onClick={handleEditButtonClick} />
  301. </div>
  302. </div>
  303. <div className="relative mt-6">
  304. {dataIsLoading ? (
  305. <div className="mt-3">
  306. <Loadbar theme="light" />
  307. </div>
  308. ) : (
  309. <div className="table-responsive mb-10">
  310. <table className="w-full table-auto bg-darkGray text-left">
  311. <thead>
  312. {table.getHeaderGroups().map((headerGroup) => (
  313. <tr key={headerGroup.id}>
  314. {headerGroup.headers.map((header) => (
  315. <th
  316. key={header.id}
  317. className={
  318. header.column.id === 'account'
  319. ? 'base16 pt-8 pb-6 pl-4 pr-2 font-semibold opacity-60 xl:pl-14'
  320. : 'base16 pt-8 pb-6 pl-1 pr-2 font-semibold opacity-60'
  321. }
  322. >
  323. {header.isPlaceholder
  324. ? null
  325. : flexRender(
  326. header.column.columnDef.header,
  327. header.getContext()
  328. )}
  329. </th>
  330. ))}
  331. </tr>
  332. ))}
  333. </thead>
  334. <tbody>
  335. {table.getRowModel().rows.map((row) => (
  336. <tr key={row.id} className="border-t border-beige-300">
  337. {row.getVisibleCells().map((cell) => (
  338. <td
  339. key={cell.id}
  340. onBlur={(e) =>
  341. handleEditPubkey(
  342. e,
  343. cell.row.original.account,
  344. cell.row.original.pubkey
  345. )
  346. }
  347. contentEditable={
  348. cell.column.id === 'newPubkey' && editable
  349. ? true
  350. : false
  351. }
  352. suppressContentEditableWarning={true}
  353. className={
  354. cell.column.id === 'account'
  355. ? 'py-3 pl-4 pr-2 xl:pl-14'
  356. : 'items-center py-3 pl-1 pr-4'
  357. }
  358. >
  359. {flexRender(
  360. cell.column.columnDef.cell,
  361. cell.getContext()
  362. )}
  363. </td>
  364. ))}
  365. </tr>
  366. ))}
  367. </tbody>
  368. </table>
  369. </div>
  370. )}
  371. </div>
  372. </div>
  373. </div>
  374. )
  375. }
  376. export default UpdatePermissions