Browse Source

[xc-admin] add feature to send proposal to update permissions (#498)

* fix wallet hydration error

* update package-lock.json

* add edit feature

* [wip] sqds proposal

* [wip] fix multisig hook

* checkpoint

* update UI

* fix error

* refactor

* make Modal reusable

* address comments
Daniel Chew 2 years ago
parent
commit
88d8039c36

+ 50 - 2
governance/xc-admin/package-lock.json

@@ -8670,6 +8670,37 @@
         "tslib": "^2.4.0"
       }
     },
+    "node_modules/@tanstack/react-table": {
+      "version": "8.7.6",
+      "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.7.6.tgz",
+      "integrity": "sha512-/QijmMFeP7wDLBnr0MQ/5MlbXePbIL/1nOtkxBC9zvmBu4gDKJEDBqipUyM7Wc/iBpSd0IFyqBlvZvTPD9FYDA==",
+      "dependencies": {
+        "@tanstack/table-core": "8.7.6"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/tannerlinsley"
+      },
+      "peerDependencies": {
+        "react": ">=16",
+        "react-dom": ">=16"
+      }
+    },
+    "node_modules/@tanstack/table-core": {
+      "version": "8.7.6",
+      "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.7.6.tgz",
+      "integrity": "sha512-sqiNTMzB6cpyL8DFH6/VqW48SwiflLqxQqYpo2wNock7rdVGvlm0BLNI8vZUJbr1+fmmWmHwBvi5OMgZw8n1DA==",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/tannerlinsley"
+      }
+    },
     "node_modules/@terra-money/legacy.proto": {
       "name": "@terra-money/terra.proto",
       "version": "0.1.7",
@@ -25244,12 +25275,14 @@
       "dependencies": {
         "@coral-xyz/anchor": "^0.26.0",
         "@headlessui/react": "^1.7.7",
-        "@pythnetwork/client": "^2.9.0",
+        "@pythnetwork/client": "^2.10.0",
         "@solana/wallet-adapter-base": "^0.9.20",
+        "@solana/wallet-adapter-react": "^0.15.28",
         "@solana/wallet-adapter-react-ui": "^0.9.27",
         "@solana/wallet-adapter-wallets": "^0.19.10",
         "@solana/web3.js": "^1.73.0",
         "@sqds/mesh": "^1.0.6",
+        "@tanstack/react-table": "^8.7.6",
         "@types/node": "18.11.18",
         "@types/react": "18.0.26",
         "@types/react-dom": "18.0.10",
@@ -30830,6 +30863,19 @@
         "tslib": "^2.4.0"
       }
     },
+    "@tanstack/react-table": {
+      "version": "8.7.6",
+      "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.7.6.tgz",
+      "integrity": "sha512-/QijmMFeP7wDLBnr0MQ/5MlbXePbIL/1nOtkxBC9zvmBu4gDKJEDBqipUyM7Wc/iBpSd0IFyqBlvZvTPD9FYDA==",
+      "requires": {
+        "@tanstack/table-core": "8.7.6"
+      }
+    },
+    "@tanstack/table-core": {
+      "version": "8.7.6",
+      "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.7.6.tgz",
+      "integrity": "sha512-sqiNTMzB6cpyL8DFH6/VqW48SwiflLqxQqYpo2wNock7rdVGvlm0BLNI8vZUJbr1+fmmWmHwBvi5OMgZw8n1DA=="
+    },
     "@terra-money/legacy.proto": {
       "version": "npm:@terra-money/terra.proto@0.1.7",
       "requires": {
@@ -41997,13 +42043,15 @@
       "requires": {
         "@coral-xyz/anchor": "^0.26.0",
         "@headlessui/react": "^1.7.7",
-        "@pythnetwork/client": "^2.9.0",
+        "@pythnetwork/client": "^2.10.0",
         "@solana/wallet-adapter-base": "^0.9.20",
+        "@solana/wallet-adapter-react": "^0.15.28",
         "@solana/wallet-adapter-react-ui": "^0.9.27",
         "@solana/wallet-adapter-wallets": "^0.19.10",
         "@solana/web3.js": "^1.73.0",
         "@sqds/mesh": "^1.0.6",
         "@svgr/webpack": "^6.3.1",
+        "@tanstack/react-table": "^8.7.6",
         "@types/node": "18.11.18",
         "@types/react": "18.0.26",
         "@types/react-dom": "18.0.10",

+ 18 - 0
governance/xc-admin/packages/xc-admin-frontend/components/EditButton.tsx

@@ -0,0 +1,18 @@
+const EditButton = ({
+  editable,
+  onClick,
+}: {
+  editable?: boolean
+  onClick: React.MouseEventHandler<HTMLButtonElement>
+}) => {
+  return (
+    <button
+      className={`bg-darkGray2 py-3 px-6 text-sm font-semibold uppercase outline-none transition-colors`}
+      onClick={onClick}
+    >
+      <span>{editable ? 'done' : 'edit'}</span>
+    </button>
+  )
+}
+
+export default EditButton

+ 102 - 0
governance/xc-admin/packages/xc-admin-frontend/components/common/Modal.tsx

@@ -0,0 +1,102 @@
+import { Dialog, Transition } from '@headlessui/react'
+import { Dispatch, Fragment, SetStateAction } from 'react'
+import CloseIcon from '../icons/CloseIcon'
+import Spinner from './Spinner'
+
+const Modal: React.FC<{
+  isModalOpen: boolean
+  setIsModalOpen: Dispatch<SetStateAction<boolean>>
+  closeModal: () => void
+  changes: any
+  handleSendProposalButtonClick: () => void
+  isSendProposalButtonLoading: boolean
+}> = ({
+  isModalOpen,
+  setIsModalOpen,
+  closeModal,
+  changes,
+  handleSendProposalButtonClick,
+  isSendProposalButtonLoading,
+}) => {
+  return (
+    <Transition appear show={isModalOpen} as={Fragment}>
+      <Dialog
+        as="div"
+        className="relative z-10"
+        onClose={() => setIsModalOpen(false)}
+      >
+        <Transition.Child
+          as={Fragment}
+          enter="ease-out duration-300"
+          enterFrom="opacity-0"
+          enterTo="opacity-100"
+          leave="ease-in duration-200"
+          leaveFrom="opacity-100"
+          leaveTo="opacity-0"
+        >
+          <div className="fixed inset-0 bg-black bg-opacity-50" />
+        </Transition.Child>
+        <div className="fixed inset-0 overflow-y-auto">
+          <div className="flex min-h-full items-center justify-center p-4 text-center">
+            <Transition.Child
+              as={Fragment}
+              enter="ease-out duration-300"
+              enterFrom="opacity-0 scale-95"
+              enterTo="opacity-100 scale-100"
+              leave="ease-in duration-200"
+              leaveFrom="opacity-100 scale-100"
+              leaveTo="opacity-0 scale-95"
+            >
+              <Dialog.Panel className="diaglogPanel">
+                <button className="diaglogClose" onClick={closeModal}>
+                  <span className="mr-3">close</span> <CloseIcon />
+                </button>
+                <div className="max-w-full">
+                  <Dialog.Title as="h3" className="diaglogTitle">
+                    Proposed Changes
+                  </Dialog.Title>
+
+                  {!changes ? (
+                    <p className="mb-8 leading-6 ">No proposed changes.</p>
+                  ) : (
+                    Object.keys(changes).map((key) => {
+                      if (changes[key].prev !== changes[key].new) {
+                        return (
+                          <div
+                            key={key}
+                            className="flex items-center justify-between pb-4"
+                          >
+                            <span className="pr-4 text-left font-bold">
+                              {key}
+                            </span>
+                            <span className="mr-2">
+                              {changes[key].prev} &rarr; {changes[key].new}
+                            </span>
+                          </div>
+                        )
+                      }
+                    })
+                  )}
+
+                  <button
+                    className="action-btn text-base "
+                    onClick={handleSendProposalButtonClick}
+                    disabled={!changes}
+                  >
+                    {isSendProposalButtonLoading ? (
+                      <Spinner />
+                    ) : (
+                      'Send Proposal'
+                    )}
+                  </button>
+                </div>
+              </Dialog.Panel>
+            </Transition.Child>
+          </div>
+        </div>
+      </Dialog>
+    </Transition>
+  )
+}
+
+export default Modal

+ 27 - 0
governance/xc-admin/packages/xc-admin-frontend/components/common/Spinner.tsx

@@ -0,0 +1,27 @@
+import React from 'react'
+
+const Spinner = () => {
+  return (
+    <svg
+      className="inline-block h-4  w-4 animate-spin "
+      xmlns="http://www.w3.org/2000/svg"
+      fill="none"
+      viewBox="0 0 24 24"
+    >
+      <circle
+        className="opacity-25"
+        cx={12}
+        cy={12}
+        r={10}
+        stroke="currentColor"
+        strokeWidth={3}
+      />
+      <path
+        className="opacity-75"
+        fill="currentColor"
+        d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
+      />
+    </svg>
+  )
+}
+export default Spinner

+ 18 - 0
governance/xc-admin/packages/xc-admin-frontend/components/icons/CloseIcon.tsx

@@ -0,0 +1,18 @@
+const CloseIcon = () => {
+  return (
+    <svg
+      width={13}
+      height={13}
+      viewBox="0 0 13 13"
+      fill="none"
+      xmlns="http://www.w3.org/2000/svg"
+    >
+      <path
+        d="M12.1934 0L6.5 5.69343L0.806569 0L0 0.806569L5.69343 6.5L0 12.1934L0.806569 13L6.5 7.30657L12.1934 13L13 12.1934L7.30657 6.5L13 0.806569L12.1934 0Z"
+        fill="#E6DAFE"
+      />
+    </svg>
+  )
+}
+
+export default CloseIcon

+ 19 - 7
governance/xc-admin/packages/xc-admin-frontend/components/layout/Header.tsx

@@ -1,4 +1,4 @@
-import { WalletMultiButton } from '@solana/wallet-adapter-react-ui'
+import dynamic from 'next/dynamic'
 import Link from 'next/link'
 import { useRouter } from 'next/router'
 import { useContext, useEffect, useState } from 'react'
@@ -6,12 +6,18 @@ import { ClusterContext, DEFAULT_CLUSTER } from '../../contexts/ClusterContext'
 import Pyth from '../../images/logomark.inline.svg'
 import MobileMenu from './MobileMenu'
 
+const WalletMultiButtonDynamic = dynamic(
+  async () =>
+    (await import('@solana/wallet-adapter-react-ui')).WalletMultiButton,
+  { ssr: false }
+)
+
 export interface BurgerState {
   initial: boolean | null
   opened: boolean | null
 }
 
-function Header() {
+const Header = () => {
   const { cluster } = useContext(ClusterContext)
   const router = useRouter()
   const [isSticky, setIsSticky] = useState(false)
@@ -78,7 +84,7 @@ function Header() {
         >
           <Link href="/">
             <a
-              className={`flex min-h-[45px] basis-[160px] cursor-pointer items-center`}
+              className={`flex min-h-[45px] basis-[180px] cursor-pointer items-center`}
             >
               <Pyth />
             </a>
@@ -107,11 +113,17 @@ function Header() {
             </ul>
           </nav>
           <div className="flex items-center justify-end space-x-2">
-            {headerState.opened ? null : (
-              <WalletMultiButton className="primary-btn pt-0.5" />
-            )}
+            <div className="h-[45px] w-[180px]">
+              {headerState.opened ? null : (
+                <WalletMultiButtonDynamic className="primary-btn float-right pt-0.5" />
+              )}
+            </div>
             <div
-              className={`relative top-0 right-5 left-0 basis-7
+              className={`${
+                headerState.opened
+                  ? 'relative top-0 right-5 left-0 basis-7'
+                  : 'lg:hidden'
+              }
               `}
               onClick={handleToggleMenu}
             >

+ 3 - 1
governance/xc-admin/packages/xc-admin-frontend/components/layout/Layout.tsx

@@ -2,7 +2,7 @@ import React from 'react'
 import Footer from './Footer'
 import Header from './Header'
 
-export default function Layout({ children }: { children: React.ReactNode }) {
+const Layout = ({ children }: { children: React.ReactNode }) => {
   return (
     <div className="relative overflow-hidden">
       <Header />
@@ -11,3 +11,5 @@ export default function Layout({ children }: { children: React.ReactNode }) {
     </div>
   )
 }
+
+export default Layout

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

@@ -62,7 +62,7 @@ const MinPublishers = () => {
                       )
                   ) : (
                     <tr className="border-t border-beige-300">
-                      <td className="py-3 pl-1 lg:pl-14" colSpan={2}>
+                      <td className="py-3 pl-4 lg:pl-14" colSpan={2}>
                         No mapping accounts found.
                       </td>
                     </tr>

+ 358 - 53
governance/xc-admin/packages/xc-admin-frontend/components/tabs/UpdatePermissions.tsx

@@ -1,88 +1,393 @@
+import { AnchorProvider, Program, Wallet } from '@coral-xyz/anchor'
+import {
+  getPythProgramKeyForCluster,
+  pythOracleProgram,
+} from '@pythnetwork/client'
+import { PythOracle } from '@pythnetwork/client/lib/anchor'
+import { useAnchorWallet, useWallet } from '@solana/wallet-adapter-react'
 import { PublicKey } from '@solana/web3.js'
+import {
+  createColumnHelper,
+  flexRender,
+  getCoreRowModel,
+  useReactTable,
+} from '@tanstack/react-table'
 import copy from 'copy-to-clipboard'
-import React from 'react'
+import { useContext, useEffect, useState } from 'react'
+import toast from 'react-hot-toast'
+import { proposeInstructions } from 'xc-admin-common'
+import { ClusterContext } from '../../contexts/ClusterContext'
 import { usePythContext } from '../../contexts/PythContext'
+import {
+  getMultisigCluster,
+  UPGRADE_MUTLTISIG,
+  useMultisig,
+} from '../../hooks/useMultisig'
 import CopyIcon from '../../images/icons/copy.inline.svg'
+import { capitalizeFirstLetter } from '../../utils/capitalizeFirstLetter'
 import ClusterSwitch from '../ClusterSwitch'
+import Modal from '../common/Modal'
+import EditButton from '../EditButton'
 import Loadbar from '../loaders/Loadbar'
 
 interface UpdatePermissionsProps {
-  account: string
-  pubkey?: PublicKey
+  account: PermissionAccount
+  pubkey: string
+  newPubkey?: string
 }
 
-const UpdatePermissionsRow: React.FunctionComponent<UpdatePermissionsProps> = ({
-  account,
-  pubkey = new PublicKey(0),
-}) => {
-  return (
-    <tr key={account} className="border-t border-beige-300">
-      <td className="py-3 pl-4 pr-2 lg:pl-14">{account}</td>
-      <td className="py-3 pl-1 lg:pl-14">
-        <div
-          className="-ml-1 inline-flex cursor-pointer items-center px-1 hover:bg-dark hover:text-white active:bg-darkGray3"
-          onClick={() => {
-            copy(pubkey.toBase58())
-          }}
-        >
-          <span className="mr-2 hidden lg:block">{pubkey.toBase58()}</span>
-          <span className="mr-2 lg:hidden">
-            {pubkey.toBase58().slice(0, 6) +
-              '...' +
-              pubkey.toBase58().slice(-6)}
-          </span>{' '}
-          <CopyIcon className="shrink-0" />
-        </div>
-      </td>
-    </tr>
-  )
+const DEFAULT_DATA: UpdatePermissionsProps[] = [
+  {
+    account: 'Master Authority',
+    pubkey: new PublicKey(0).toBase58(),
+  },
+  {
+    account: 'Data Curation Authority',
+    pubkey: new PublicKey(0).toBase58(),
+  },
+  {
+    account: 'Security Authority',
+    pubkey: new PublicKey(0).toBase58(),
+  },
+]
+
+const BPF_UPGRADABLE_LOADER = new PublicKey(
+  'BPFLoaderUpgradeab1e11111111111111111111111'
+)
+
+const columnHelper = createColumnHelper<UpdatePermissionsProps>()
+
+const defaultColumns = [
+  columnHelper.accessor('account', {
+    cell: (info) => info.getValue(),
+    header: () => <span>Account</span>,
+  }),
+  columnHelper.accessor('pubkey', {
+    cell: (props) => {
+      const pubkey = props.getValue()
+      return (
+        <>
+          <div
+            className="-ml-1 inline-flex cursor-pointer items-center px-1 hover:bg-dark hover:text-white active:bg-darkGray3"
+            onClick={() => {
+              copy(pubkey)
+            }}
+          >
+            <span className="mr-2 hidden lg:block">{pubkey}</span>
+            <span className="mr-2 lg:hidden">
+              {pubkey.slice(0, 6) + '...' + pubkey.slice(-6)}
+            </span>{' '}
+            <CopyIcon className="shrink-0" />
+          </div>
+        </>
+      )
+    },
+    header: () => <span>Public Key</span>,
+  }),
+]
+
+// make a type with 3 possible values
+type PermissionAccount =
+  | 'Master Authority'
+  | 'Data Curation Authority'
+  | 'Security Authority'
+
+interface PermissionAccountInfo {
+  prev: string
+  new: string
 }
 
 const UpdatePermissions = () => {
-  const { rawConfig, dataIsLoading } = usePythContext()
+  const [data, setData] = useState(() => [...DEFAULT_DATA])
+  const [columns, setColumns] = useState(() => [...defaultColumns])
+  const [pubkeyChanges, setPubkeyChanges] =
+    useState<Partial<Record<PermissionAccount, PermissionAccountInfo>>>()
+  const [finalPubkeyChanges, setFinalPubkeyChanges] =
+    useState<Record<PermissionAccount, PermissionAccountInfo>>()
+  const [editable, setEditable] = useState(false)
+  const [isModalOpen, setIsModalOpen] = useState(false)
+  const [isSendProposalButtonLoading, setIsSendProposalButtonLoading] =
+    useState(false)
+  const { cluster } = useContext(ClusterContext)
+  const anchorWallet = useAnchorWallet()
+  const { isLoading: isMultisigLoading, squads } = useMultisig(
+    anchorWallet as Wallet
+  )
+  const { rawConfig, dataIsLoading, connection } = usePythContext()
+  const { connected } = useWallet()
+  const [pythProgramClient, setPythProgramClient] =
+    useState<Program<PythOracle>>()
+
+  useEffect(() => {
+    if (rawConfig.permissionAccount) {
+      const masterAuthority =
+        rawConfig.permissionAccount.masterAuthority.toBase58()
+      const dataCurationAuthority =
+        rawConfig.permissionAccount.dataCurationAuthority.toBase58()
+      const securityAuthority =
+        rawConfig.permissionAccount.securityAuthority.toBase58()
+      setData([
+        {
+          account: 'Master Authority',
+          pubkey: masterAuthority,
+        },
+        {
+          account: 'Data Curation Authority',
+          pubkey: dataCurationAuthority,
+        },
+        {
+          account: 'Security Authority',
+          pubkey: securityAuthority,
+        },
+      ])
+    }
+  }, [dataIsLoading, rawConfig])
+
+  const table = useReactTable({
+    data,
+    columns,
+    getCoreRowModel: getCoreRowModel(),
+  })
+
+  const backfillPubkeyChanges = () => {
+    const newPubkeyChanges: Record<PermissionAccount, PermissionAccountInfo> = {
+      'Master Authority': {
+        prev: data[0].pubkey,
+        new: data[0].pubkey,
+      },
+      'Data Curation Authority': {
+        prev: data[1].pubkey,
+        new: data[1].pubkey,
+      },
+      'Security Authority': {
+        prev: data[2].pubkey,
+        new: data[2].pubkey,
+      },
+    }
+    if (pubkeyChanges) {
+      Object.keys(pubkeyChanges).forEach((key) => {
+        newPubkeyChanges[key as PermissionAccount] = pubkeyChanges[
+          key as PermissionAccount
+        ] as PermissionAccountInfo
+      })
+    }
+
+    return newPubkeyChanges
+  }
+
+  //   let newPubkeyChanges: Record<PermissionAccount, PermissionAccountInfo>
+  //   data.forEach((d) => {
+  //     if (!newPubkeyChanges[d.account]) {
+  //       newPubkeyChanges[d.account] = {
+  //         prev: d.pubkey,
+  //         new: d.pubkey,
+  //       }
+  //     }
+  //   })
+
+  // return newPubkeyChanges
+
+  const handleEditButtonClick = () => {
+    const nextState = !editable
+    if (nextState) {
+      setColumns([
+        ...defaultColumns,
+        columnHelper.accessor('newPubkey', {
+          cell: (info) => info.getValue(),
+          header: () => <span>New Public Key</span>,
+        }),
+      ])
+    } else {
+      if (pubkeyChanges && Object.keys(pubkeyChanges).length > 0) {
+        openModal()
+        setFinalPubkeyChanges(backfillPubkeyChanges())
+      } else {
+        setColumns(defaultColumns)
+      }
+    }
+    setEditable(nextState)
+  }
+
+  const openModal = () => {
+    setIsModalOpen(true)
+  }
+
+  const closeModal = () => {
+    setIsModalOpen(false)
+  }
+
+  // check if pubkey is valid
+  const isValidPubkey = (pubkey: string) => {
+    try {
+      new PublicKey(pubkey)
+      return true
+    } catch (e) {
+      return false
+    }
+  }
+
+  const handleEditPubkey = (
+    e: any,
+    account: PermissionAccount,
+    prevPubkey: string
+  ) => {
+    const newPubkey = e.target.textContent
+    if (isValidPubkey(newPubkey) && newPubkey !== prevPubkey) {
+      setPubkeyChanges({
+        ...pubkeyChanges,
+        [account]: {
+          prev: prevPubkey,
+          new: newPubkey,
+        },
+      })
+    }
+  }
+
+  const handleSendProposalButtonClick = () => {
+    if (pythProgramClient && finalPubkeyChanges) {
+      const programDataAccount = PublicKey.findProgramAddressSync(
+        [pythProgramClient?.programId.toBuffer()],
+        BPF_UPGRADABLE_LOADER
+      )[0]
+      pythProgramClient?.methods
+        .updPermissions(
+          new PublicKey(finalPubkeyChanges['Master Authority'].new),
+          new PublicKey(finalPubkeyChanges['Data Curation Authority'].new),
+          new PublicKey(finalPubkeyChanges['Security Authority'].new)
+        )
+        .accounts({
+          upgradeAuthority: squads?.getAuthorityPDA(
+            UPGRADE_MUTLTISIG[getMultisigCluster(cluster)],
+            1
+          ),
+          programDataAccount,
+        })
+        .instruction()
+        .then(async (instruction) => {
+          if (!isMultisigLoading && squads) {
+            setIsSendProposalButtonLoading(true)
+            try {
+              const proposalPubkey = await proposeInstructions(
+                squads,
+                UPGRADE_MUTLTISIG[getMultisigCluster(cluster)],
+                [instruction],
+                false
+              )
+              toast.success(
+                `Proposal sent! 🚀 Proposal Pubkey: ${proposalPubkey}`
+              )
+              setIsSendProposalButtonLoading(false)
+            } catch (e: any) {
+              toast.error(capitalizeFirstLetter(e.message))
+              setIsSendProposalButtonLoading(false)
+            }
+          }
+        })
+    }
+  }
+
+  // create anchor wallet when connected
+  useEffect(() => {
+    if (connected) {
+      const provider = new AnchorProvider(
+        connection,
+        anchorWallet as Wallet,
+        AnchorProvider.defaultOptions()
+      )
+      setPythProgramClient(
+        pythOracleProgram(getPythProgramKeyForCluster(cluster), provider)
+      )
+    }
+  }, [anchorWallet, connection, connected, cluster])
 
   return (
     <div className="relative">
+      <Modal
+        isModalOpen={isModalOpen}
+        setIsModalOpen={setIsModalOpen}
+        closeModal={closeModal}
+        changes={pubkeyChanges}
+        handleSendProposalButtonClick={handleSendProposalButtonClick}
+        isSendProposalButtonLoading={isSendProposalButtonLoading}
+      />
       <div className="container flex flex-col items-center justify-between lg:flex-row">
         <div className="mb-4 w-full text-left lg:mb-0">
           <h1 className="h1 mb-4">Update Permissions</h1>
         </div>
       </div>
       <div className="container">
-        <div className="mb-4 md:mb-0">
-          <ClusterSwitch />
+        <div className="flex justify-between">
+          <div className="mb-4 md:mb-0">
+            <ClusterSwitch />
+          </div>
+          <div className="mb-4 md:mb-0">
+            <EditButton editable={editable} onClick={handleEditButtonClick} />
+          </div>
         </div>
-        <div className="table-responsive relative mt-6">
+        <div className="relative mt-6">
           {dataIsLoading ? (
             <div className="mt-3">
               <Loadbar theme="light" />
             </div>
           ) : (
             <div className="table-responsive mb-10">
-              <table className="w-full bg-darkGray text-left">
+              <table className="w-full table-auto bg-darkGray text-left">
                 <thead>
-                  <tr>
-                    <th className="base16 pt-8 pb-6 pl-4 pr-2 font-semibold opacity-60 lg:pl-14">
-                      Account
-                    </th>
-                    <th className="base16 pt-8 pb-6 pl-1 pr-2 font-semibold opacity-60 lg:pl-14">
-                      Public Key
-                    </th>
-                  </tr>
+                  {table.getHeaderGroups().map((headerGroup) => (
+                    <tr key={headerGroup.id}>
+                      {headerGroup.headers.map((header) => (
+                        <th
+                          key={header.id}
+                          className={
+                            header.column.id === 'account'
+                              ? 'base16 pt-8 pb-6 pl-4 pr-2 font-semibold opacity-60 xl:pl-14'
+                              : 'base16 pt-8 pb-6 pl-1 pr-2 font-semibold opacity-60'
+                          }
+                        >
+                          {header.isPlaceholder
+                            ? null
+                            : flexRender(
+                                header.column.columnDef.header,
+                                header.getContext()
+                              )}
+                        </th>
+                      ))}
+                    </tr>
+                  ))}
                 </thead>
                 <tbody>
-                  <UpdatePermissionsRow
-                    account="Master Authority"
-                    pubkey={rawConfig.permissionAccount?.masterAuthority}
-                  />
-                  <UpdatePermissionsRow
-                    account="Data Curation Authority"
-                    pubkey={rawConfig.permissionAccount?.dataCurationAuthority}
-                  />
-                  <UpdatePermissionsRow
-                    account="Security Authority"
-                    pubkey={rawConfig.permissionAccount?.securityAuthority}
-                  />
+                  {table.getRowModel().rows.map((row) => (
+                    <tr key={row.id} className="border-t border-beige-300">
+                      {row.getVisibleCells().map((cell) => (
+                        <td
+                          key={cell.id}
+                          onBlur={(e) =>
+                            handleEditPubkey(
+                              e,
+                              cell.row.original.account,
+                              cell.row.original.pubkey
+                            )
+                          }
+                          contentEditable={
+                            cell.column.id === 'newPubkey' && editable
+                              ? true
+                              : false
+                          }
+                          suppressContentEditableWarning={true}
+                          className={
+                            cell.column.id === 'account'
+                              ? 'py-3 pl-4 pr-2 xl:pl-14'
+                              : 'items-center py-3 pl-1 pr-4'
+                          }
+                        >
+                          {flexRender(
+                            cell.column.columnDef.cell,
+                            cell.getContext()
+                          )}
+                        </td>
+                      ))}
+                    </tr>
+                  ))}
                 </tbody>
               </table>
             </div>

+ 6 - 1
governance/xc-admin/packages/xc-admin-frontend/contexts/MultisigContext.tsx

@@ -1,3 +1,5 @@
+import { Wallet } from '@coral-xyz/anchor'
+import { useAnchorWallet } from '@solana/wallet-adapter-react'
 import SquadsMesh from '@sqds/mesh'
 import { TransactionAccount } from '@sqds/mesh/lib/types'
 import React, { createContext, useContext, useMemo } from 'react'
@@ -27,7 +29,10 @@ interface MultisigContextProviderProps {
 export const MultisigContextProvider: React.FC<
   MultisigContextProviderProps
 > = ({ children }) => {
-  const { isLoading, error, squads, proposals } = useMultisig()
+  const anchorWallet = useAnchorWallet()
+  const { isLoading, error, squads, proposals } = useMultisig(
+    anchorWallet as Wallet
+  )
 
   const value = useMemo(
     () => ({

+ 30 - 26
governance/xc-admin/packages/xc-admin-frontend/hooks/useMultisig.ts

@@ -1,6 +1,6 @@
 import { Wallet } from '@coral-xyz/anchor'
 import { PythCluster } from '@pythnetwork/client/lib/cluster'
-import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js'
+import { Cluster, Connection, PublicKey } from '@solana/web3.js'
 import SquadsMesh from '@sqds/mesh'
 import { TransactionAccount } from '@sqds/mesh/lib/types'
 import { useContext, useEffect, useRef, useState } from 'react'
@@ -33,7 +33,7 @@ interface MultisigHookData {
   proposals: TransactionAccount[]
 }
 
-export const useMultisig = (): MultisigHookData => {
+export const useMultisig = (wallet: Wallet): MultisigHookData => {
   const connectionRef = useRef<Connection>()
   const { cluster } = useContext(ClusterContext)
   const [isLoading, setIsLoading] = useState(true)
@@ -57,37 +57,41 @@ export const useMultisig = (): MultisigHookData => {
 
     connectionRef.current = connection
     ;(async () => {
-      try {
-        const squads = new SquadsMesh({
-          connection,
-          wallet: new Keypair() as unknown as Wallet,
-        })
-        setProposals(
-          await getProposals(
-            squads,
-            UPGRADE_MUTLTISIG[getMultisigCluster(cluster)]
+      if (wallet) {
+        try {
+          const squads = new SquadsMesh({
+            connection,
+            wallet,
+          })
+          setProposals(
+            await getProposals(
+              squads,
+              UPGRADE_MUTLTISIG[getMultisigCluster(cluster)]
+            )
           )
-        )
-        setSquads(squads)
-        setIsLoading(false)
-      } catch (e) {
-        if (cancelled) return
-        if (urlsIndex === urls.length - 1) {
-          // @ts-ignore
-          setError(e)
+          setSquads(squads)
           setIsLoading(false)
-          console.warn(`Failed to fetch accounts`)
-        } else if (urlsIndex < urls.length - 1) {
-          setUrlsIndex((urlsIndex) => urlsIndex + 1)
-          console.warn(
-            `Failed with ${urls[urlsIndex]}, trying with ${urls[urlsIndex + 1]}`
-          )
+        } catch (e) {
+          if (cancelled) return
+          if (urlsIndex === urls.length - 1) {
+            // @ts-ignore
+            setError(e)
+            setIsLoading(false)
+            console.warn(`Failed to fetch accounts`)
+          } else if (urlsIndex < urls.length - 1) {
+            setUrlsIndex((urlsIndex) => urlsIndex + 1)
+            console.warn(
+              `Failed with ${urls[urlsIndex]}, trying with ${
+                urls[urlsIndex + 1]
+              }`
+            )
+          }
         }
       }
     })()
 
     return () => {}
-  }, [urlsIndex, cluster])
+  }, [urlsIndex, cluster, wallet])
 
   return {
     isLoading,

+ 3 - 1
governance/xc-admin/packages/xc-admin-frontend/package.json

@@ -11,12 +11,14 @@
   "dependencies": {
     "@coral-xyz/anchor": "^0.26.0",
     "@headlessui/react": "^1.7.7",
-    "@pythnetwork/client": "^2.9.0",
+    "@pythnetwork/client": "^2.10.0",
     "@solana/wallet-adapter-base": "^0.9.20",
+    "@solana/wallet-adapter-react": "^0.15.28",
     "@solana/wallet-adapter-react-ui": "^0.9.27",
     "@solana/wallet-adapter-wallets": "^0.19.10",
     "@solana/web3.js": "^1.73.0",
     "@sqds/mesh": "^1.0.6",
+    "@tanstack/react-table": "^8.7.6",
     "@types/node": "18.11.18",
     "@types/react": "18.0.26",
     "@types/react-dom": "18.0.10",

+ 16 - 0
governance/xc-admin/packages/xc-admin-frontend/styles/globals.css

@@ -268,3 +268,19 @@
 .wallet-adapter-button.secondary-btn:hover {
   @apply hover:bg-pythPurple;
 }
+
+.diaglogPanel {
+  @apply flex h-full min-h-[420px]  w-[calc(100%-24px)]  max-w-6xl transform items-center justify-center rounded-[40px] bg-[rgba(49,47,71,1)] p-5 px-6 pt-20 pb-8 text-center  align-middle shadow-xl transition-all md:mt-[92px] lg:p-10;
+}
+
+.diaglogClose {
+  @apply absolute right-10 top-8  flex items-center leading-none;
+}
+
+.diaglogTitle {
+  @apply mb-8 text-center font-body text-[32px] leading-[1.1] lg:mb-11 lg:text-[44px];
+}
+
+.action-btn {
+  @apply h-[45px] rounded-full bg-pythPurple  px-8 font-mono  font-semibold uppercase  leading-none transition-colors  hover:bg-mediumSlateBlue disabled:opacity-70 disabled:hover:bg-pythPurple;
+}

+ 1 - 0
governance/xc-admin/packages/xc-admin-frontend/tailwind.config.js

@@ -83,6 +83,7 @@ module.exports = {
         lightPurple: '#7731EA',
         offPurple: '#745E9D',
         pythPurple: '#7142CF',
+        mediumSlateBlue: '#8246FA',
       },
       letterSpacing: {
         wide: '.02em',

+ 3 - 0
governance/xc-admin/packages/xc-admin-frontend/utils/capitalizeFirstLetter.ts

@@ -0,0 +1,3 @@
+export const capitalizeFirstLetter = (str: string) => {
+  return str.replace(/^\w/, (c: string) => c.toUpperCase())
+}

+ 6 - 0
package-lock.json

@@ -0,0 +1,6 @@
+{
+  "name": "pyth-crosschain",
+  "lockfileVersion": 2,
+  "requires": true,
+  "packages": {}
+}