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

This commit is contained in:
Daniel Chew 2023-02-23 17:43:24 +09:00 committed by GitHub
parent 0abf0e2b6c
commit 8cb720c476
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 310 additions and 11 deletions

View File

@ -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

View File

@ -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

View File

@ -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}

View File

@ -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">

View File

@ -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",

View File

@ -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
package-lock.json generated
View File

@ -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",