[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 (
|
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 }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<Menu.Button
|
<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"
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo="opacity-0 scale-95"
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="diaglogPanel">
|
<Dialog.Panel className="dialogPanel">
|
||||||
<button className="diaglogClose" onClick={closeModal}>
|
<button className="dialogClose" onClick={closeModal}>
|
||||||
<span className="mr-3">close</span> <CloseIcon />
|
<span className="mr-3">close</span> <CloseIcon />
|
||||||
</button>
|
</button>
|
||||||
<div className="max-w-full">
|
<div className="max-w-full">
|
||||||
<Dialog.Title as="h3" className="diaglogTitle">
|
<Dialog.Title as="h3" className="dialogTitle">
|
||||||
Proposed Changes
|
Proposed Changes
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
{content}
|
{content}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import ClusterSwitch from '../ClusterSwitch'
|
||||||
import Modal from '../common/Modal'
|
import Modal from '../common/Modal'
|
||||||
import Spinner from '../common/Spinner'
|
import Spinner from '../common/Spinner'
|
||||||
import Loadbar from '../loaders/Loadbar'
|
import Loadbar from '../loaders/Loadbar'
|
||||||
|
import PermissionDepermissionKey from '../PermissionDepermissionKey'
|
||||||
|
|
||||||
const General = () => {
|
const General = () => {
|
||||||
const [data, setData] = useState<any>({})
|
const [data, setData] = useState<any>({})
|
||||||
|
@ -689,6 +690,18 @@ const General = () => {
|
||||||
<ClusterSwitch />
|
<ClusterSwitch />
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div className="relative mt-6">
|
||||||
{dataIsLoading ? (
|
{dataIsLoading ? (
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
|
|
|
@ -12,8 +12,9 @@
|
||||||
"@coral-xyz/anchor": "^0.26.0",
|
"@coral-xyz/anchor": "^0.26.0",
|
||||||
"@headlessui/react": "^1.7.7",
|
"@headlessui/react": "^1.7.7",
|
||||||
"@pythnetwork/client": "^2.15.0",
|
"@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",
|
"@radix-ui/react-tooltip": "^1.0.3",
|
||||||
|
"@solana/spl-token": "^0.3.7",
|
||||||
"@solana/wallet-adapter-base": "^0.9.20",
|
"@solana/wallet-adapter-base": "^0.9.20",
|
||||||
"@solana/wallet-adapter-react": "^0.15.28",
|
"@solana/wallet-adapter-react": "^0.15.28",
|
||||||
"@solana/wallet-adapter-react-ui": "^0.9.27",
|
"@solana/wallet-adapter-react-ui": "^0.9.27",
|
||||||
|
|
|
@ -269,16 +269,16 @@
|
||||||
@apply hover:bg-pythPurple;
|
@apply hover:bg-pythPurple;
|
||||||
}
|
}
|
||||||
|
|
||||||
.diaglogPanel {
|
.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;
|
@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 {
|
.dialogClose {
|
||||||
@apply absolute right-10 top-8 flex items-center leading-none;
|
@apply absolute right-10 top-8 flex items-center leading-none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.diaglogTitle {
|
.dialogTitle {
|
||||||
@apply mb-8 text-center font-body text-[32px] leading-[1.1] lg:mb-11 lg:text-[44px];
|
@apply mb-8 text-center font-body text-[32px] leading-[1.1] lg:mb-11 lg:text-[44px] px-10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn {
|
.action-btn {
|
||||||
|
|
|
@ -1416,6 +1416,7 @@
|
||||||
"@coral-xyz/anchor": "^0.26.0",
|
"@coral-xyz/anchor": "^0.26.0",
|
||||||
"@headlessui/react": "^1.7.7",
|
"@headlessui/react": "^1.7.7",
|
||||||
"@pythnetwork/client": "^2.15.0",
|
"@pythnetwork/client": "^2.15.0",
|
||||||
|
"@radix-ui/react-label": "^2.0.0",
|
||||||
"@radix-ui/react-tooltip": "^1.0.3",
|
"@radix-ui/react-tooltip": "^1.0.3",
|
||||||
"@solana/spl-token": "^0.3.7",
|
"@solana/spl-token": "^0.3.7",
|
||||||
"@solana/wallet-adapter-base": "^0.9.20",
|
"@solana/wallet-adapter-base": "^0.9.20",
|
||||||
|
@ -11064,6 +11065,19 @@
|
||||||
"react": "^16.8 || ^17.0 || ^18.0"
|
"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": {
|
"node_modules/@radix-ui/react-popper": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.0.tgz",
|
"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-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": {
|
"@radix-ui/react-popper": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.0.tgz",
|
"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",
|
"@coral-xyz/anchor": "^0.26.0",
|
||||||
"@headlessui/react": "^1.7.7",
|
"@headlessui/react": "^1.7.7",
|
||||||
"@pythnetwork/client": "^2.15.0",
|
"@pythnetwork/client": "^2.15.0",
|
||||||
|
"@radix-ui/react-label": "^2.0.0",
|
||||||
"@radix-ui/react-tooltip": "^1.0.3",
|
"@radix-ui/react-tooltip": "^1.0.3",
|
||||||
"@solana/spl-token": "^0.3.7",
|
"@solana/spl-token": "^0.3.7",
|
||||||
"@solana/wallet-adapter-base": "^0.9.20",
|
"@solana/wallet-adapter-base": "^0.9.20",
|
||||||
|
|
Loading…
Reference in New Issue