Переглянути джерело

[xc-admin] add permission/depermission publisher keys for all asset types (#620)

Daniel Chew 2 роки тому
батько
коміт
8cb720c476

+ 1 - 1
governance/xc_admin/packages/xc_admin_frontend/components/ClusterSwitch.tsx

@@ -57,7 +57,7 @@ const ClusterSwitch = ({ light }: { light?: boolean | null }) => {
   ]
 
   return (
-    <Menu as="div" className="relative z-[2] block w-[180px] text-left">
+    <Menu as="div" className="relative z-[3] block w-[180px] text-left">
       {({ open }) => (
         <>
           <Menu.Button

+ 261 - 0
governance/xc_admin/packages/xc_admin_frontend/components/PermissionDepermissionKey.tsx

@@ -0,0 +1,261 @@
+import { Program } from '@coral-xyz/anchor'
+import { Dialog, Menu, Transition } from '@headlessui/react'
+import { PythOracle } from '@pythnetwork/client/lib/anchor'
+import * as Label from '@radix-ui/react-label'
+import { useWallet } from '@solana/wallet-adapter-react'
+import { WalletModalButton } from '@solana/wallet-adapter-react-ui'
+import { Cluster, PublicKey, TransactionInstruction } from '@solana/web3.js'
+import SquadsMesh from '@sqds/mesh'
+import { Fragment, useContext, useEffect, useState } from 'react'
+import toast from 'react-hot-toast'
+import {
+  getMultisigCluster,
+  isRemoteCluster,
+  mapKey,
+  proposeInstructions,
+  WORMHOLE_ADDRESS,
+} from 'xc_admin_common'
+import { ClusterContext } from '../contexts/ClusterContext'
+import { usePythContext } from '../contexts/PythContext'
+import { PRICE_FEED_MULTISIG } from '../hooks/useMultisig'
+import { ProductRawConfig } from '../hooks/usePyth'
+import Arrow from '../images/icons/down.inline.svg'
+import { capitalizeFirstLetter } from '../utils/capitalizeFirstLetter'
+import Spinner from './common/Spinner'
+import CloseIcon from './icons/CloseIcon'
+
+const assetTypes = ['All', 'Crypto', 'Equity', 'FX', 'Metal']
+
+const PermissionDepermissionKey = ({
+  isPermission,
+  pythProgramClient,
+  squads,
+}: {
+  isPermission: boolean
+  pythProgramClient?: Program<PythOracle>
+  squads?: SquadsMesh
+}) => {
+  const [publisherKey, setPublisherKey] = useState(
+    'JTmFx5zX9mM94itfk2nQcJnQQDPjcv4UPD7SYj6xDCV'
+  )
+  const [selectedAssetType, setSelectedAssetType] = useState('All')
+  const [isModalOpen, setIsModalOpen] = useState(false)
+  const [isSubmitButtonLoading, setIsSubmitButtonLoading] = useState(false)
+  const [priceAccounts, setPriceAccounts] = useState<PublicKey[]>([])
+  const { cluster } = useContext(ClusterContext)
+  const { rawConfig, dataIsLoading } = usePythContext()
+  const { connected } = useWallet()
+
+  // get current input value
+
+  const handleChange = (event: any) => {
+    setSelectedAssetType(event.target.value)
+    setIsModalOpen(true)
+  }
+
+  const closeModal = () => {
+    setIsModalOpen(false)
+  }
+
+  const onKeyChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
+    const {
+      currentTarget: { value },
+    } = event
+    setPublisherKey(value)
+  }
+
+  const handleSubmitButton = async () => {
+    if (pythProgramClient && squads) {
+      const instructions: TransactionInstruction[] = []
+      const multisigAuthority = squads.getAuthorityPDA(
+        PRICE_FEED_MULTISIG[getMultisigCluster(cluster)],
+        1
+      )
+      const isRemote: boolean = isRemoteCluster(cluster)
+      const multisigCluster: Cluster | 'localnet' = getMultisigCluster(cluster)
+      const wormholeAddress = WORMHOLE_ADDRESS[multisigCluster]
+      const fundingAccount = isRemote
+        ? mapKey(multisigAuthority)
+        : multisigAuthority
+      priceAccounts.map((priceAccount) => {
+        isPermission
+          ? pythProgramClient.methods
+              .addPublisher(new PublicKey(publisherKey))
+              .accounts({
+                fundingAccount,
+                priceAccount: priceAccount,
+              })
+              .instruction()
+              .then((instruction) => instructions.push(instruction))
+          : pythProgramClient.methods
+              .delPublisher(new PublicKey(publisherKey))
+              .accounts({
+                fundingAccount,
+                priceAccount: priceAccount,
+              })
+              .instruction()
+              .then((instruction) => instructions.push(instruction))
+      })
+      setIsSubmitButtonLoading(true)
+      try {
+        const proposalPubkey = await proposeInstructions(
+          squads,
+          PRICE_FEED_MULTISIG[getMultisigCluster(cluster)],
+          instructions,
+          isRemote,
+          wormholeAddress
+        )
+        toast.success(`Proposal sent! 🚀 Proposal Pubkey: ${proposalPubkey}`)
+        setIsSubmitButtonLoading(false)
+        closeModal()
+      } catch (e: any) {
+        toast.error(capitalizeFirstLetter(e.message))
+        setIsSubmitButtonLoading(false)
+      }
+    }
+  }
+
+  useEffect(() => {
+    if (!dataIsLoading) {
+      const res: PublicKey[] = []
+      rawConfig.mappingAccounts[0].products.map((product: ProductRawConfig) => {
+        const publisherExists =
+          product.priceAccounts[0].publishers.find(
+            (p) => p.toBase58() === publisherKey
+          ) !== undefined
+        if (
+          (selectedAssetType === 'All' ||
+            product.metadata.asset_type === selectedAssetType) &&
+          ((isPermission &&
+            product.priceAccounts[0].publishers.length < 32 &&
+            !publisherExists) ||
+            (!isPermission && publisherExists))
+        ) {
+          res.push(product.priceAccounts[0].address)
+        }
+      })
+      setPriceAccounts(res)
+    }
+  }, [rawConfig, dataIsLoading, selectedAssetType, isPermission, publisherKey])
+
+  return (
+    <>
+      <Menu as="div" className="relative z-[2] block w-[200px] text-left">
+        {({ open }) => (
+          <>
+            <Menu.Button
+              className={`inline-flex w-full items-center justify-between rounded-lg bg-darkGray2 py-3 px-6 text-sm outline-0`}
+            >
+              <span className="mr-3">
+                {isPermission ? 'Permission Key' : 'Depermission Key'}
+              </span>
+              <Arrow className={`${open && 'rotate-180'}`} />
+            </Menu.Button>
+            <Transition
+              as={Fragment}
+              enter="transition ease-out duration-100"
+              enterFrom="transform opacity-0 scale-95"
+              enterTo="transform opacity-100 scale-100"
+              leave="transition ease-in duration-75"
+              leaveFrom="transform opacity-100 scale-100"
+              leaveTo="transform opacity-0 scale-95"
+            >
+              <Menu.Items className="absolute right-0 mt-2 w-full origin-top-right">
+                {assetTypes.map((a) => (
+                  <Menu.Item key={a}>
+                    <button
+                      className={`block w-full bg-darkGray py-3 px-6 text-left text-sm hover:bg-darkGray2`}
+                      value={a}
+                      onClick={handleChange}
+                    >
+                      {a}
+                    </button>
+                  </Menu.Item>
+                ))}
+              </Menu.Items>
+            </Transition>
+          </>
+        )}
+      </Menu>
+      <Transition appear show={isModalOpen} as={Fragment}>
+        <Dialog
+          as="div"
+          className="relative z-40"
+          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="dialogPanel">
+                  <button className="dialogClose" onClick={closeModal}>
+                    <span className="mr-3">close</span> <CloseIcon />
+                  </button>
+                  <div className="max-w-full">
+                    <Dialog.Title as="h3" className="dialogTitle">
+                      {isPermission ? 'Permission' : 'Depermission'} Publisher
+                      Key
+                    </Dialog.Title>
+                    <div className="flex items-center justify-center">
+                      <div className="rounded-full bg-light py-2 px-4 text-sm text-dark">
+                        Asset Type: {selectedAssetType}
+                      </div>
+                    </div>
+                    <div className="mt-6 block items-center justify-center space-y-2 space-x-0 lg:flex lg:space-y-0 lg:space-x-4">
+                      <Label.Root htmlFor="publisherKey">Key</Label.Root>
+                      <input
+                        className="w-full rounded-lg bg-darkGray px-4 py-2 lg:w-3/4"
+                        type="text"
+                        id="publisherKey"
+                        onChange={onKeyChange}
+                        defaultValue={publisherKey}
+                      />
+                    </div>
+                    <div className="mt-6">
+                      {!connected ? (
+                        <div className="flex justify-center">
+                          <WalletModalButton className="action-btn text-base" />
+                        </div>
+                      ) : (
+                        <button
+                          className="action-btn text-base"
+                          onClick={handleSubmitButton}
+                        >
+                          {isSubmitButtonLoading ? (
+                            <Spinner />
+                          ) : (
+                            'Submit Proposal'
+                          )}
+                        </button>
+                      )}
+                    </div>
+                  </div>
+                </Dialog.Panel>
+              </Transition.Child>
+            </div>
+          </div>
+        </Dialog>
+      </Transition>
+    </>
+  )
+}
+
+export default PermissionDepermissionKey

+ 3 - 3
governance/xc_admin/packages/xc_admin_frontend/components/common/Modal.tsx

@@ -37,12 +37,12 @@ const Modal: React.FC<{
               leaveFrom="opacity-100 scale-100"
               leaveTo="opacity-0 scale-95"
             >
-              <Dialog.Panel className="diaglogPanel">
-                <button className="diaglogClose" onClick={closeModal}>
+              <Dialog.Panel className="dialogPanel">
+                <button className="dialogClose" onClick={closeModal}>
                   <span className="mr-3">close</span> <CloseIcon />
                 </button>
                 <div className="max-w-full">
-                  <Dialog.Title as="h3" className="diaglogTitle">
+                  <Dialog.Title as="h3" className="dialogTitle">
                     Proposed Changes
                   </Dialog.Title>
                   {content}

+ 13 - 0
governance/xc_admin/packages/xc_admin_frontend/components/tabs/General.tsx

@@ -22,6 +22,7 @@ import ClusterSwitch from '../ClusterSwitch'
 import Modal from '../common/Modal'
 import Spinner from '../common/Spinner'
 import Loadbar from '../loaders/Loadbar'
+import PermissionDepermissionKey from '../PermissionDepermissionKey'
 
 const General = () => {
   const [data, setData] = useState<any>({})
@@ -689,6 +690,18 @@ const General = () => {
             <ClusterSwitch />
           </div>
         </div>
+        <div className="relative mt-6 flex space-x-4">
+          <PermissionDepermissionKey
+            isPermission={true}
+            pythProgramClient={pythProgramClient}
+            squads={squads}
+          />
+          <PermissionDepermissionKey
+            isPermission={false}
+            pythProgramClient={pythProgramClient}
+            squads={squads}
+          />
+        </div>
         <div className="relative mt-6">
           {dataIsLoading ? (
             <div className="mt-3">

+ 2 - 1
governance/xc_admin/packages/xc_admin_frontend/package.json

@@ -12,8 +12,9 @@
     "@coral-xyz/anchor": "^0.26.0",
     "@headlessui/react": "^1.7.7",
     "@pythnetwork/client": "^2.15.0",
-    "@solana/spl-token": "^0.3.7",
+    "@radix-ui/react-label": "^2.0.0",
     "@radix-ui/react-tooltip": "^1.0.3",
+    "@solana/spl-token": "^0.3.7",
     "@solana/wallet-adapter-base": "^0.9.20",
     "@solana/wallet-adapter-react": "^0.15.28",
     "@solana/wallet-adapter-react-ui": "^0.9.27",

+ 6 - 6
governance/xc_admin/packages/xc_admin_frontend/styles/globals.css

@@ -269,16 +269,16 @@
   @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;
+.dialogPanel {
+  @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;
+.dialogClose {
+  @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];
+.dialogTitle {
+  @apply mb-8 text-center font-body text-[32px] leading-[1.1] lg:mb-11 lg:text-[44px] px-10;
 }
 
 .action-btn {

+ 24 - 0
package-lock.json

@@ -1416,6 +1416,7 @@
         "@coral-xyz/anchor": "^0.26.0",
         "@headlessui/react": "^1.7.7",
         "@pythnetwork/client": "^2.15.0",
+        "@radix-ui/react-label": "^2.0.0",
         "@radix-ui/react-tooltip": "^1.0.3",
         "@solana/spl-token": "^0.3.7",
         "@solana/wallet-adapter-base": "^0.9.20",
@@ -11064,6 +11065,19 @@
         "react": "^16.8 || ^17.0 || ^18.0"
       }
     },
+    "node_modules/@radix-ui/react-label": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.0.0.tgz",
+      "integrity": "sha512-7qCcZ3j2VQspWjy+gKR4W+V/z0XueQjeiZnlPOtsyiP9HaS8bfSU7ECoI3bvvdYntQj7NElW7OAYsYRW4MQvCg==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/react-primitive": "1.0.1"
+      },
+      "peerDependencies": {
+        "react": "^16.8 || ^17.0 || ^18.0",
+        "react-dom": "^16.8 || ^17.0 || ^18.0"
+      }
+    },
     "node_modules/@radix-ui/react-popper": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.0.tgz",
@@ -56486,6 +56500,15 @@
         "@radix-ui/react-use-layout-effect": "1.0.0"
       }
     },
+    "@radix-ui/react-label": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.0.0.tgz",
+      "integrity": "sha512-7qCcZ3j2VQspWjy+gKR4W+V/z0XueQjeiZnlPOtsyiP9HaS8bfSU7ECoI3bvvdYntQj7NElW7OAYsYRW4MQvCg==",
+      "requires": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/react-primitive": "1.0.1"
+      }
+    },
     "@radix-ui/react-popper": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.0.tgz",
@@ -85666,6 +85689,7 @@
         "@coral-xyz/anchor": "^0.26.0",
         "@headlessui/react": "^1.7.7",
         "@pythnetwork/client": "^2.15.0",
+        "@radix-ui/react-label": "^2.0.0",
         "@radix-ui/react-tooltip": "^1.0.3",
         "@solana/spl-token": "^0.3.7",
         "@solana/wallet-adapter-base": "^0.9.20",