|
| 1 | +import { Program } from '@coral-xyz/anchor' |
| 2 | +import { Dialog, Menu, Transition } from '@headlessui/react' |
| 3 | +import { PythOracle } from '@pythnetwork/client/lib/anchor' |
| 4 | +import * as Label from '@radix-ui/react-label' |
| 5 | +import { useWallet } from '@solana/wallet-adapter-react' |
| 6 | +import { WalletModalButton } from '@solana/wallet-adapter-react-ui' |
| 7 | +import { Cluster, PublicKey, TransactionInstruction } from '@solana/web3.js' |
| 8 | +import SquadsMesh from '@sqds/mesh' |
| 9 | +import { Fragment, useContext, useEffect, useState } from 'react' |
| 10 | +import toast from 'react-hot-toast' |
| 11 | +import { |
| 12 | + getMultisigCluster, |
| 13 | + isRemoteCluster, |
| 14 | + mapKey, |
| 15 | + proposeInstructions, |
| 16 | + WORMHOLE_ADDRESS, |
| 17 | +} from 'xc_admin_common' |
| 18 | +import { ClusterContext } from '../contexts/ClusterContext' |
| 19 | +import { usePythContext } from '../contexts/PythContext' |
| 20 | +import { PRICE_FEED_MULTISIG } from '../hooks/useMultisig' |
| 21 | +import { ProductRawConfig } from '../hooks/usePyth' |
| 22 | +import Arrow from '../images/icons/down.inline.svg' |
| 23 | +import { capitalizeFirstLetter } from '../utils/capitalizeFirstLetter' |
| 24 | +import Spinner from './common/Spinner' |
| 25 | +import CloseIcon from './icons/CloseIcon' |
| 26 | + |
| 27 | +const assetTypes = ['All', 'Crypto', 'Equity', 'FX', 'Metal'] |
| 28 | + |
| 29 | +const PermissionDepermissionKey = ({ |
| 30 | + isPermission, |
| 31 | + pythProgramClient, |
| 32 | + squads, |
| 33 | +}: { |
| 34 | + isPermission: boolean |
| 35 | + pythProgramClient?: Program<PythOracle> |
| 36 | + squads?: SquadsMesh |
| 37 | +}) => { |
| 38 | + const [publisherKey, setPublisherKey] = useState( |
| 39 | + 'JTmFx5zX9mM94itfk2nQcJnQQDPjcv4UPD7SYj6xDCV' |
| 40 | + ) |
| 41 | + const [selectedAssetType, setSelectedAssetType] = useState('All') |
| 42 | + const [isModalOpen, setIsModalOpen] = useState(false) |
| 43 | + const [isSubmitButtonLoading, setIsSubmitButtonLoading] = useState(false) |
| 44 | + const [priceAccounts, setPriceAccounts] = useState<PublicKey[]>([]) |
| 45 | + const { cluster } = useContext(ClusterContext) |
| 46 | + const { rawConfig, dataIsLoading } = usePythContext() |
| 47 | + const { connected } = useWallet() |
| 48 | + |
| 49 | + // get current input value |
| 50 | + |
| 51 | + const handleChange = (event: any) => { |
| 52 | + setSelectedAssetType(event.target.value) |
| 53 | + setIsModalOpen(true) |
| 54 | + } |
| 55 | + |
| 56 | + const closeModal = () => { |
| 57 | + setIsModalOpen(false) |
| 58 | + } |
| 59 | + |
| 60 | + const onKeyChange = (event: React.SyntheticEvent<HTMLInputElement>) => { |
| 61 | + const { |
| 62 | + currentTarget: { value }, |
| 63 | + } = event |
| 64 | + setPublisherKey(value) |
| 65 | + } |
| 66 | + |
| 67 | + const handleSubmitButton = async () => { |
| 68 | + if (pythProgramClient && squads) { |
| 69 | + const instructions: TransactionInstruction[] = [] |
| 70 | + const multisigAuthority = squads.getAuthorityPDA( |
| 71 | + PRICE_FEED_MULTISIG[getMultisigCluster(cluster)], |
| 72 | + 1 |
| 73 | + ) |
| 74 | + const isRemote: boolean = isRemoteCluster(cluster) |
| 75 | + const multisigCluster: Cluster | 'localnet' = getMultisigCluster(cluster) |
| 76 | + const wormholeAddress = WORMHOLE_ADDRESS[multisigCluster] |
| 77 | + const fundingAccount = isRemote |
| 78 | + ? mapKey(multisigAuthority) |
| 79 | + : multisigAuthority |
| 80 | + priceAccounts.map((priceAccount) => { |
| 81 | + isPermission |
| 82 | + ? pythProgramClient.methods |
| 83 | + .addPublisher(new PublicKey(publisherKey)) |
| 84 | + .accounts({ |
| 85 | + fundingAccount, |
| 86 | + priceAccount: priceAccount, |
| 87 | + }) |
| 88 | + .instruction() |
| 89 | + .then((instruction) => instructions.push(instruction)) |
| 90 | + : pythProgramClient.methods |
| 91 | + .delPublisher(new PublicKey(publisherKey)) |
| 92 | + .accounts({ |
| 93 | + fundingAccount, |
| 94 | + priceAccount: priceAccount, |
| 95 | + }) |
| 96 | + .instruction() |
| 97 | + .then((instruction) => instructions.push(instruction)) |
| 98 | + }) |
| 99 | + setIsSubmitButtonLoading(true) |
| 100 | + try { |
| 101 | + const proposalPubkey = await proposeInstructions( |
| 102 | + squads, |
| 103 | + PRICE_FEED_MULTISIG[getMultisigCluster(cluster)], |
| 104 | + instructions, |
| 105 | + isRemote, |
| 106 | + wormholeAddress |
| 107 | + ) |
| 108 | + toast.success(`Proposal sent! 🚀 Proposal Pubkey: ${proposalPubkey}`) |
| 109 | + setIsSubmitButtonLoading(false) |
| 110 | + closeModal() |
| 111 | + } catch (e: any) { |
| 112 | + toast.error(capitalizeFirstLetter(e.message)) |
| 113 | + setIsSubmitButtonLoading(false) |
| 114 | + } |
| 115 | + } |
| 116 | + } |
| 117 | + |
| 118 | + useEffect(() => { |
| 119 | + if (!dataIsLoading) { |
| 120 | + const res: PublicKey[] = [] |
| 121 | + rawConfig.mappingAccounts[0].products.map((product: ProductRawConfig) => { |
| 122 | + const publisherExists = |
| 123 | + product.priceAccounts[0].publishers.find( |
| 124 | + (p) => p.toBase58() === publisherKey |
| 125 | + ) !== undefined |
| 126 | + if ( |
| 127 | + (selectedAssetType === 'All' || |
| 128 | + product.metadata.asset_type === selectedAssetType) && |
| 129 | + ((isPermission && |
| 130 | + product.priceAccounts[0].publishers.length < 32 && |
| 131 | + !publisherExists) || |
| 132 | + (!isPermission && publisherExists)) |
| 133 | + ) { |
| 134 | + res.push(product.priceAccounts[0].address) |
| 135 | + } |
| 136 | + }) |
| 137 | + setPriceAccounts(res) |
| 138 | + } |
| 139 | + }, [rawConfig, dataIsLoading, selectedAssetType, isPermission, publisherKey]) |
| 140 | + |
| 141 | + return ( |
| 142 | + <> |
| 143 | + <Menu as="div" className="relative z-[2] block w-[200px] text-left"> |
| 144 | + {({ open }) => ( |
| 145 | + <> |
| 146 | + <Menu.Button |
| 147 | + className={`inline-flex w-full items-center justify-between rounded-lg bg-darkGray2 py-3 px-6 text-sm outline-0`} |
| 148 | + > |
| 149 | + <span className="mr-3"> |
| 150 | + {isPermission ? 'Permission Key' : 'Depermission Key'} |
| 151 | + </span> |
| 152 | + <Arrow className={`${open && 'rotate-180'}`} /> |
| 153 | + </Menu.Button> |
| 154 | + <Transition |
| 155 | + as={Fragment} |
| 156 | + enter="transition ease-out duration-100" |
| 157 | + enterFrom="transform opacity-0 scale-95" |
| 158 | + enterTo="transform opacity-100 scale-100" |
| 159 | + leave="transition ease-in duration-75" |
| 160 | + leaveFrom="transform opacity-100 scale-100" |
| 161 | + leaveTo="transform opacity-0 scale-95" |
| 162 | + > |
| 163 | + <Menu.Items className="absolute right-0 mt-2 w-full origin-top-right"> |
| 164 | + {assetTypes.map((a) => ( |
| 165 | + <Menu.Item key={a}> |
| 166 | + <button |
| 167 | + className={`block w-full bg-darkGray py-3 px-6 text-left text-sm hover:bg-darkGray2`} |
| 168 | + value={a} |
| 169 | + onClick={handleChange} |
| 170 | + > |
| 171 | + {a} |
| 172 | + </button> |
| 173 | + </Menu.Item> |
| 174 | + ))} |
| 175 | + </Menu.Items> |
| 176 | + </Transition> |
| 177 | + </> |
| 178 | + )} |
| 179 | + </Menu> |
| 180 | + <Transition appear show={isModalOpen} as={Fragment}> |
| 181 | + <Dialog |
| 182 | + as="div" |
| 183 | + className="relative z-40" |
| 184 | + onClose={() => setIsModalOpen(false)} |
| 185 | + > |
| 186 | + <Transition.Child |
| 187 | + as={Fragment} |
| 188 | + enter="ease-out duration-300" |
| 189 | + enterFrom="opacity-0" |
| 190 | + enterTo="opacity-100" |
| 191 | + leave="ease-in duration-200" |
| 192 | + leaveFrom="opacity-100" |
| 193 | + leaveTo="opacity-0" |
| 194 | + > |
| 195 | + <div className="fixed inset-0 bg-black bg-opacity-50" /> |
| 196 | + </Transition.Child> |
| 197 | + <div className="fixed inset-0 overflow-y-auto"> |
| 198 | + <div className="flex min-h-full items-center justify-center p-4 text-center"> |
| 199 | + <Transition.Child |
| 200 | + as={Fragment} |
| 201 | + enter="ease-out duration-300" |
| 202 | + enterFrom="opacity-0 scale-95" |
| 203 | + enterTo="opacity-100 scale-100" |
| 204 | + leave="ease-in duration-200" |
| 205 | + leaveFrom="opacity-100 scale-100" |
| 206 | + leaveTo="opacity-0 scale-95" |
| 207 | + > |
| 208 | + <Dialog.Panel className="dialogPanel"> |
| 209 | + <button className="dialogClose" onClick={closeModal}> |
| 210 | + <span className="mr-3">close</span> <CloseIcon /> |
| 211 | + </button> |
| 212 | + <div className="max-w-full"> |
| 213 | + <Dialog.Title as="h3" className="dialogTitle"> |
| 214 | + {isPermission ? 'Permission' : 'Depermission'} Publisher |
| 215 | + Key |
| 216 | + </Dialog.Title> |
| 217 | + <div className="flex items-center justify-center"> |
| 218 | + <div className="rounded-full bg-light py-2 px-4 text-sm text-dark"> |
| 219 | + Asset Type: {selectedAssetType} |
| 220 | + </div> |
| 221 | + </div> |
| 222 | + <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"> |
| 223 | + <Label.Root htmlFor="publisherKey">Key</Label.Root> |
| 224 | + <input |
| 225 | + className="w-full rounded-lg bg-darkGray px-4 py-2 lg:w-3/4" |
| 226 | + type="text" |
| 227 | + id="publisherKey" |
| 228 | + onChange={onKeyChange} |
| 229 | + defaultValue={publisherKey} |
| 230 | + /> |
| 231 | + </div> |
| 232 | + <div className="mt-6"> |
| 233 | + {!connected ? ( |
| 234 | + <div className="flex justify-center"> |
| 235 | + <WalletModalButton className="action-btn text-base" /> |
| 236 | + </div> |
| 237 | + ) : ( |
| 238 | + <button |
| 239 | + className="action-btn text-base" |
| 240 | + onClick={handleSubmitButton} |
| 241 | + > |
| 242 | + {isSubmitButtonLoading ? ( |
| 243 | + <Spinner /> |
| 244 | + ) : ( |
| 245 | + 'Submit Proposal' |
| 246 | + )} |
| 247 | + </button> |
| 248 | + )} |
| 249 | + </div> |
| 250 | + </div> |
| 251 | + </Dialog.Panel> |
| 252 | + </Transition.Child> |
| 253 | + </div> |
| 254 | + </div> |
| 255 | + </Dialog> |
| 256 | + </Transition> |
| 257 | + </> |
| 258 | + ) |
| 259 | +} |
| 260 | + |
| 261 | +export default PermissionDepermissionKey |
0 commit comments