[xc-admin] add permission/depermission publisher keys for all asset types (#620)
This commit is contained in:
parent
0abf0e2b6c
commit
8cb720c476
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue