From e841989fa23a70183127613bd187e92493662845 Mon Sep 17 00:00:00 2001 From: Daniel Chew Date: Tue, 14 Feb 2023 20:55:39 +0900 Subject: [PATCH] [xc-admin] remove unused tabs (#596) --- .../components/tabs/AddRemovePublishers.tsx | 391 ------------------ .../components/tabs/MinPublishers.tsx | 361 ---------------- .../components/tabs/UpdateProductMetadata.tsx | 362 ---------------- .../xc_admin_frontend/pages/index.tsx | 28 -- 4 files changed, 1142 deletions(-) delete mode 100644 governance/xc_admin/packages/xc_admin_frontend/components/tabs/AddRemovePublishers.tsx delete mode 100644 governance/xc_admin/packages/xc_admin_frontend/components/tabs/MinPublishers.tsx delete mode 100644 governance/xc_admin/packages/xc_admin_frontend/components/tabs/UpdateProductMetadata.tsx diff --git a/governance/xc_admin/packages/xc_admin_frontend/components/tabs/AddRemovePublishers.tsx b/governance/xc_admin/packages/xc_admin_frontend/components/tabs/AddRemovePublishers.tsx deleted file mode 100644 index 8cd9d066..00000000 --- a/governance/xc_admin/packages/xc_admin_frontend/components/tabs/AddRemovePublishers.tsx +++ /dev/null @@ -1,391 +0,0 @@ -import { AnchorProvider, Program, Wallet } from '@coral-xyz/anchor' -import { - getPythProgramKeyForCluster, - pythOracleProgram, -} from '@pythnetwork/client' -import { PythOracle } from '@pythnetwork/client/lib/anchor' -import { useAnchorWallet, useWallet } from '@solana/wallet-adapter-react' -import { WalletModalButton } from '@solana/wallet-adapter-react-ui' -import { PublicKey, TransactionInstruction } from '@solana/web3.js' -import { Fragment, useContext, useEffect, useState } from 'react' -import toast from 'react-hot-toast' -import { getMultisigCluster, proposeInstructions } from 'xc_admin_common' -import { ClusterContext } from '../../contexts/ClusterContext' -import { usePythContext } from '../../contexts/PythContext' -import { PRICE_FEED_MULTISIG, useMultisig } from '../../hooks/useMultisig' -import { capitalizeFirstLetter } from '../../utils/capitalizeFirstLetter' -import ClusterSwitch from '../ClusterSwitch' -import Modal from '../common/Modal' -import Spinner from '../common/Spinner' -import Loadbar from '../loaders/Loadbar' - -interface SymbolToPublisherKeys { - [key: string]: PublicKey[] -} - -interface PublishersInfo { - prev: string[] - new: string[] -} - -let symbolToPriceAccountKeyMapping: Record = {} - -const AddRemovePublishers = () => { - const [data, setData] = useState({}) - const [publisherChanges, setPublisherChanges] = - useState>() - const [isModalOpen, setIsModalOpen] = useState(false) - const [isSendProposalButtonLoading, setIsSendProposalButtonLoading] = - useState(false) - const { cluster } = useContext(ClusterContext) - const anchorWallet = useAnchorWallet() - const { isLoading: isMultisigLoading, squads } = useMultisig( - anchorWallet as Wallet - ) - const { rawConfig, dataIsLoading, connection } = usePythContext() - const { connected } = useWallet() - const [pythProgramClient, setPythProgramClient] = - useState>() - - const openModal = () => { - setIsModalOpen(true) - } - - const closeModal = () => { - setIsModalOpen(false) - } - - useEffect(() => { - if (!dataIsLoading && rawConfig && rawConfig.mappingAccounts.length > 0) { - let symbolToPublisherKeysMapping: SymbolToPublisherKeys = {} - rawConfig.mappingAccounts.map((mappingAccount) => { - mappingAccount.products.map((product) => { - const priceAccount = product.priceAccounts.find( - (priceAccount) => - priceAccount.address.toBase58() === product.metadata.price_account - ) - if (priceAccount) { - symbolToPublisherKeysMapping[product.metadata.symbol] = - priceAccount.publishers - symbolToPriceAccountKeyMapping[product.metadata.symbol] = - priceAccount.address.toBase58() - } - }) - }) - symbolToPublisherKeysMapping = sortData(symbolToPublisherKeysMapping) - setData(symbolToPublisherKeysMapping) - } - }, [rawConfig, dataIsLoading]) - - const sortData = (data: SymbolToPublisherKeys) => { - let sortedSymbolToPublisherKeysMapping: SymbolToPublisherKeys = {} - // sort symbolToPublisherKeysMapping by symbol - sortedSymbolToPublisherKeysMapping = JSON.parse( - JSON.stringify(data, Object.keys(data).sort()) - ) - // sort symbolToPublisherKeysMapping by publisher keys - Object.keys(sortedSymbolToPublisherKeysMapping).forEach((key) => { - // sort publisher keys and make them each of type PublicKey because JSON.stringify makes them of type string - sortedSymbolToPublisherKeysMapping[key] = - sortedSymbolToPublisherKeysMapping[key] - .sort() - .map((publisherKey) => new PublicKey(publisherKey)) - }) - return sortedSymbolToPublisherKeysMapping - } - - // function to download json file - const handleDownloadJsonButtonClick = () => { - const dataStr = - 'data:text/json;charset=utf-8,' + - encodeURIComponent(JSON.stringify(data, null, 2)) - const downloadAnchor = document.createElement('a') - downloadAnchor.setAttribute('href', dataStr) - downloadAnchor.setAttribute('download', 'publishers.json') - document.body.appendChild(downloadAnchor) // required for firefox - downloadAnchor.click() - downloadAnchor.remove() - } - - // function to upload json file and update publisherChanges state - const handleUploadJsonButtonClick = () => { - const uploadAnchor = document.createElement('input') - uploadAnchor.setAttribute('type', 'file') - uploadAnchor.setAttribute('accept', '.json') - uploadAnchor.addEventListener('change', (e) => { - const file = (e.target as HTMLInputElement).files![0] - const reader = new FileReader() - reader.onload = (e) => { - if (e.target) { - const fileData = e.target.result - if (!isValidJson(fileData as string)) return - const fileDataParsed = sortData(JSON.parse(fileData as string)) - const changes: Record = {} - Object.keys(fileDataParsed).forEach((symbol) => { - if ( - JSON.stringify(data[symbol]) !== - JSON.stringify(fileDataParsed[symbol]) - ) { - changes[symbol] = { prev: [], new: [] } - changes[symbol].prev = data[symbol].map((p: PublicKey) => - p.toBase58() - ) - changes[symbol].new = fileDataParsed[symbol].map((p: PublicKey) => - p.toBase58() - ) - } - }) - setPublisherChanges(changes) - openModal() - } - } - reader.readAsText(file) - }) - document.body.appendChild(uploadAnchor) // required for firefox - uploadAnchor.click() - uploadAnchor.remove() - } - - // check if uploaded json is valid json - const isValidJson = (json: string) => { - try { - JSON.parse(json) - } catch (e: any) { - toast.error(capitalizeFirstLetter(e.message)) - return false - } - // check if json keys are existing products - const jsonParsed = JSON.parse(json) - const jsonSymbols = Object.keys(jsonParsed) - const existingSymbols = Object.keys(data) - // check that jsonSymbols is equal to existingSymbols no matter the order - if ( - JSON.stringify(jsonSymbols.sort()) !== - JSON.stringify(existingSymbols.sort()) - ) { - toast.error('Symbols in json file do not match existing symbols!') - return false - } - return true - } - - const handleSendProposalButtonClick = async () => { - if (pythProgramClient && publisherChanges) { - const instructions: TransactionInstruction[] = [] - Object.keys(publisherChanges).forEach((symbol) => { - const { prev, new: newPublisherKeys } = publisherChanges[symbol] - // prev and new are arrays of publisher pubkeys - // check if there are any new publishers to add by comparing prev and new - const publisherKeysToAdd = newPublisherKeys.filter( - (newPublisher) => !prev.includes(newPublisher) - ) - // check if there are any publishers to remove by comparing prev and new - const publisherKeysToRemove = prev.filter( - (prevPublisher) => !newPublisherKeys.includes(prevPublisher) - ) - // add instructions to add new publishers - publisherKeysToAdd.forEach((publisherKey) => { - pythProgramClient.methods - .addPublisher(new PublicKey(publisherKey)) - .accounts({ - fundingAccount: squads?.getAuthorityPDA( - PRICE_FEED_MULTISIG[getMultisigCluster(cluster)], - 1 - ), - priceAccount: new PublicKey( - symbolToPriceAccountKeyMapping[symbol] - ), - }) - .instruction() - .then((instruction) => instructions.push(instruction)) - }) - // add instructions to remove publishers - publisherKeysToRemove.forEach((publisherKey) => { - pythProgramClient.methods - .delPublisher(new PublicKey(publisherKey)) - .accounts({ - fundingAccount: squads?.getAuthorityPDA( - PRICE_FEED_MULTISIG[getMultisigCluster(cluster)], - 1 - ), - priceAccount: new PublicKey( - symbolToPriceAccountKeyMapping[symbol] - ), - }) - .instruction() - .then((instruction) => instructions.push(instruction)) - }) - }) - if (!isMultisigLoading && squads) { - setIsSendProposalButtonLoading(true) - try { - const proposalPubkey = await proposeInstructions( - squads, - PRICE_FEED_MULTISIG[getMultisigCluster(cluster)], - instructions, - false - ) - toast.success(`Proposal sent! 🚀 Proposal Pubkey: ${proposalPubkey}`) - setIsSendProposalButtonLoading(false) - } catch (e: any) { - toast.error(capitalizeFirstLetter(e.message)) - setIsSendProposalButtonLoading(false) - } - } - } - } - - const ModalContent = ({ changes }: { changes: any }) => { - return ( - <> - {Object.keys(changes).length > 0 ? ( - - - - - - - - {Object.keys(changes).map((key) => { - const publisherKeysToAdd = changes[key].new.filter( - (newPublisher: string) => - !changes[key].prev.includes(newPublisher) - ) - const publisherKeysToRemove = changes[key].prev.filter( - (prevPublisher: string) => - !changes[key].new.includes(prevPublisher) - ) - return ( - changes[key].prev !== changes[key].new && ( - - - - - - - {publisherKeysToAdd.length > 0 && ( - - - - - )} - {publisherKeysToRemove.length > 0 && ( - - - - - )} - - - ) - ) - })} -
- Description - - ID -
Product{key}
- Add Publisher(s) - - {publisherKeysToAdd.map((publisherKey: string) => ( - - {publisherKey} - - ))} -
- Remove Publisher(s) - - {publisherKeysToRemove.map( - (publisherKey: string) => ( - - {publisherKey} - - ) - )} -
- ) : ( -

No proposed changes.

- )} - {Object.keys(changes).length > 0 ? ( - !connected ? ( -
- -
- ) : ( - - ) - ) : null} - - ) - } - - // create anchor wallet when connected - useEffect(() => { - if (connected) { - const provider = new AnchorProvider( - connection, - anchorWallet as Wallet, - AnchorProvider.defaultOptions() - ) - setPythProgramClient( - pythOracleProgram(getPythProgramKeyForCluster(cluster), provider) - ) - } - }, [anchorWallet, connection, connected, cluster]) - - return ( -
- } - /> -
-
-

Add/Remove Publishers

-
-
-
-
-
- -
-
-
- {dataIsLoading ? ( -
- -
- ) : ( -
-
- -
-
- -
-
- )} -
-
-
- ) -} - -export default AddRemovePublishers diff --git a/governance/xc_admin/packages/xc_admin_frontend/components/tabs/MinPublishers.tsx b/governance/xc_admin/packages/xc_admin_frontend/components/tabs/MinPublishers.tsx deleted file mode 100644 index 028d555d..00000000 --- a/governance/xc_admin/packages/xc_admin_frontend/components/tabs/MinPublishers.tsx +++ /dev/null @@ -1,361 +0,0 @@ -import { AnchorProvider, Program, Wallet } from '@coral-xyz/anchor' -import { - getPythProgramKeyForCluster, - pythOracleProgram, -} from '@pythnetwork/client' -import { PythOracle } from '@pythnetwork/client/lib/anchor' -import { useAnchorWallet, useWallet } from '@solana/wallet-adapter-react' -import { WalletModalButton } from '@solana/wallet-adapter-react-ui' -import { TransactionInstruction } from '@solana/web3.js' -import { - createColumnHelper, - flexRender, - getCoreRowModel, - useReactTable, -} from '@tanstack/react-table' -import { useContext, useEffect, useState } from 'react' -import toast from 'react-hot-toast' -import { getMultisigCluster, proposeInstructions } from 'xc_admin_common' -import { ClusterContext } from '../../contexts/ClusterContext' -import { usePythContext } from '../../contexts/PythContext' -import { PRICE_FEED_MULTISIG, useMultisig } from '../../hooks/useMultisig' -import { capitalizeFirstLetter } from '../../utils/capitalizeFirstLetter' -import ClusterSwitch from '../ClusterSwitch' -import Modal from '../common/Modal' -import Spinner from '../common/Spinner' -import EditButton from '../EditButton' -import Loadbar from '../loaders/Loadbar' - -interface MinPublishersProps { - symbol: string - minPublishers: number - newMinPublishers?: number -} - -interface MinPublishersInfo { - prev: number - new: number -} - -const columnHelper = createColumnHelper() - -const defaultColumns = [ - columnHelper.accessor('symbol', { - cell: (info) => info.getValue(), - header: () => Symbol, - }), - columnHelper.accessor('minPublishers', { - cell: (props) => { - const minPublishers = props.getValue() - return {minPublishers} - }, - header: () => Min Publishers, - }), -] - -const MinPublishers = () => { - const [data, setData] = useState([]) - const [columns, setColumns] = useState(() => [...defaultColumns]) - const [minPublishersChanges, setMinPublishersChanges] = - useState>() - const [editable, setEditable] = useState(false) - const [isModalOpen, setIsModalOpen] = useState(false) - const [isSendProposalButtonLoading, setIsSendProposalButtonLoading] = - useState(false) - const { cluster } = useContext(ClusterContext) - const anchorWallet = useAnchorWallet() - const { isLoading: isMultisigLoading, squads } = useMultisig( - anchorWallet as Wallet - ) - const { rawConfig, dataIsLoading, connection } = usePythContext() - const { connected } = useWallet() - const [pythProgramClient, setPythProgramClient] = - useState>() - - const openModal = () => { - setIsModalOpen(true) - } - - const closeModal = () => { - setIsModalOpen(false) - } - - const handleEditButtonClick = () => { - const nextState = !editable - if (nextState) { - const newColumns = [ - ...defaultColumns, - columnHelper.accessor('newMinPublishers', { - cell: (info) => info.getValue(), - header: () => New Min Publishers, - }), - ] - setColumns(newColumns) - } else { - if ( - minPublishersChanges && - Object.keys(minPublishersChanges).length > 0 - ) { - openModal() - setMinPublishersChanges(minPublishersChanges) - } else { - setColumns(defaultColumns) - } - } - - setEditable(nextState) - } - - const handleEditMinPublishers = ( - e: any, - symbol: string, - prevMinPublishers: number - ) => { - const newMinPublishers = Number(e.target.textContent) - if (prevMinPublishers !== newMinPublishers) { - setMinPublishersChanges({ - ...minPublishersChanges, - [symbol]: { - prev: prevMinPublishers, - new: newMinPublishers, - }, - }) - } else { - // delete symbol from minPublishersChanges if it exists - if (minPublishersChanges && minPublishersChanges[symbol]) { - delete minPublishersChanges[symbol] - } - setMinPublishersChanges(minPublishersChanges) - } - } - - useEffect(() => { - if (!dataIsLoading && rawConfig && rawConfig.mappingAccounts.length > 0) { - const minPublishersData: MinPublishersProps[] = [] - rawConfig.mappingAccounts - .sort( - (mapping1, mapping2) => - mapping2.products.length - mapping1.products.length - )[0] - .products.sort((product1, product2) => - product1.metadata.symbol.localeCompare(product2.metadata.symbol) - ) - .map((product) => - product.priceAccounts.map((priceAccount) => { - minPublishersData.push({ - symbol: product.metadata.symbol, - minPublishers: priceAccount.minPub, - }) - }) - ) - setData(minPublishersData) - } - }, [setData, rawConfig, dataIsLoading]) - - const table = useReactTable({ - data, - columns, - getCoreRowModel: getCoreRowModel(), - }) - - const handleSendProposalButtonClick = async () => { - if (pythProgramClient && minPublishersChanges) { - const instructions: TransactionInstruction[] = [] - Object.keys(minPublishersChanges).forEach((symbol) => { - const { prev, new: newMinPublishers } = minPublishersChanges[symbol] - const priceAccountPubkey = rawConfig.mappingAccounts - .sort( - (mapping1, mapping2) => - mapping2.products.length - mapping1.products.length - )[0] - .products.find((product) => product.metadata.symbol === symbol)! - .priceAccounts.find( - (priceAccount) => priceAccount.minPub === prev - )!.address - - pythProgramClient.methods - .setMinPub(newMinPublishers, [0, 0, 0]) - .accounts({ - priceAccount: priceAccountPubkey, - fundingAccount: squads?.getAuthorityPDA( - PRICE_FEED_MULTISIG[getMultisigCluster(cluster)], - 1 - ), - }) - .instruction() - .then((instruction) => instructions.push(instruction)) - }) - if (!isMultisigLoading && squads) { - setIsSendProposalButtonLoading(true) - try { - const proposalPubkey = await proposeInstructions( - squads, - PRICE_FEED_MULTISIG[getMultisigCluster(cluster)], - instructions, - false - ) - toast.success(`Proposal sent! 🚀 Proposal Pubkey: ${proposalPubkey}`) - setIsSendProposalButtonLoading(false) - } catch (e: any) { - toast.error(capitalizeFirstLetter(e.message)) - setIsSendProposalButtonLoading(false) - } - } - } - } - - const ModalContent = ({ changes }: { changes: any }) => { - return ( - <> - {Object.keys(changes).length > 0 ? ( -
- {Object.keys(changes).map((key) => { - return ( - changes[key].prev !== changes[key].new && ( - <> -
- {key} - - {changes[key].prev} → {changes[key].new} - -
- - ) - ) - })} -
- ) : ( -

No proposed changes.

- )} - {Object.keys(changes).length > 0 ? ( - !connected ? ( -
- -
- ) : ( - - ) - ) : null} - - ) - } - - // create anchor wallet when connected - useEffect(() => { - if (connected) { - const provider = new AnchorProvider( - connection, - anchorWallet as Wallet, - AnchorProvider.defaultOptions() - ) - setPythProgramClient( - pythOracleProgram(getPythProgramKeyForCluster(cluster), provider) - ) - } - }, [anchorWallet, connection, connected, cluster]) - - return ( -
- } - /> -
-
-

Min Publishers

-
-
-
-
-
- -
-
- -
-
-
- {dataIsLoading ? ( -
- -
- ) : ( -
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - ))} - - ))} - - - {table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - ))} - - ))} - -
- {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} -
- handleEditMinPublishers( - e, - cell.row.original.symbol, - cell.row.original.minPublishers - ) - } - contentEditable={ - cell.column.id === 'newMinPublishers' && editable - ? true - : false - } - suppressContentEditableWarning={true} - className={ - cell.column.id === 'symbol' - ? 'py-3 pl-4 pr-2 xl:pl-14' - : 'items-center py-3 pl-1 pr-4' - } - > - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} -
-
- )} -
-
-
- ) -} - -export default MinPublishers diff --git a/governance/xc_admin/packages/xc_admin_frontend/components/tabs/UpdateProductMetadata.tsx b/governance/xc_admin/packages/xc_admin_frontend/components/tabs/UpdateProductMetadata.tsx deleted file mode 100644 index 5a18abf1..00000000 --- a/governance/xc_admin/packages/xc_admin_frontend/components/tabs/UpdateProductMetadata.tsx +++ /dev/null @@ -1,362 +0,0 @@ -import { AnchorProvider, Program, Wallet } from '@coral-xyz/anchor' -import { - getPythProgramKeyForCluster, - Product, - pythOracleProgram, -} from '@pythnetwork/client' -import { PythOracle } from '@pythnetwork/client/lib/anchor' -import { useAnchorWallet, useWallet } from '@solana/wallet-adapter-react' -import { WalletModalButton } from '@solana/wallet-adapter-react-ui' -import { PublicKey, TransactionInstruction } from '@solana/web3.js' -import { useContext, useEffect, useState } from 'react' -import toast from 'react-hot-toast' -import { getMultisigCluster, proposeInstructions } from 'xc_admin_common' -import { ClusterContext } from '../../contexts/ClusterContext' -import { usePythContext } from '../../contexts/PythContext' -import { PRICE_FEED_MULTISIG, useMultisig } from '../../hooks/useMultisig' -import { capitalizeFirstLetter } from '../../utils/capitalizeFirstLetter' -import ClusterSwitch from '../ClusterSwitch' -import Modal from '../common/Modal' -import Spinner from '../common/Spinner' -import Loadbar from '../loaders/Loadbar' - -interface SymbolToProductMetadata { - [key: string]: Product -} - -interface ProductMetadataInfo { - prev: Product - new: Product -} - -const symbolToProductAccountKeyMapping: Record = {} - -const UpdateProductMetadata = () => { - const [data, setData] = useState({}) - const [productMetadataChanges, setProductMetadataChanges] = - useState>() - const [isModalOpen, setIsModalOpen] = useState(false) - const [isSendProposalButtonLoading, setIsSendProposalButtonLoading] = - useState(false) - const { cluster } = useContext(ClusterContext) - const anchorWallet = useAnchorWallet() - const { isLoading: isMultisigLoading, squads } = useMultisig( - anchorWallet as Wallet - ) - const { rawConfig, dataIsLoading, connection } = usePythContext() - const { connected } = useWallet() - const [pythProgramClient, setPythProgramClient] = - useState>() - - const openModal = () => { - setIsModalOpen(true) - } - - const closeModal = () => { - setIsModalOpen(false) - } - - useEffect(() => { - if (!dataIsLoading && rawConfig && rawConfig.mappingAccounts.length > 0) { - const symbolToProductMetadataMapping: SymbolToProductMetadata = {} - rawConfig.mappingAccounts - .sort( - (mapping1, mapping2) => - mapping2.products.length - mapping1.products.length - )[0] - .products.map((product) => { - symbolToProductAccountKeyMapping[product.metadata.symbol] = - product.address - // create copy of product.metadata to avoid mutating the original product.metadata - symbolToProductMetadataMapping[product.metadata.symbol] = { - ...product.metadata, - } - // these fields are immutable and should not be updated - delete symbolToProductMetadataMapping[product.metadata.symbol].address - delete symbolToProductMetadataMapping[product.metadata.symbol].symbol - delete symbolToProductMetadataMapping[product.metadata.symbol] - .price_account - }) - setData(sortData(symbolToProductMetadataMapping)) - } - }, [rawConfig, dataIsLoading]) - - const sortData = (data: SymbolToProductMetadata) => { - const sortedSymbolToProductMetadataMapping: SymbolToProductMetadata = {} - Object.keys(data) - .sort() - .forEach((key) => { - const sortedInnerData: any = {} - Object.keys(data[key]) - .sort() - .forEach((innerKey) => { - sortedInnerData[innerKey] = data[key][innerKey] - }) - sortedSymbolToProductMetadataMapping[key] = sortedInnerData - }) - - return sortedSymbolToProductMetadataMapping - } - - // function to download json file - const handleDownloadJsonButtonClick = () => { - const dataStr = - 'data:text/json;charset=utf-8,' + - encodeURIComponent(JSON.stringify(data, null, 2)) - const downloadAnchor = document.createElement('a') - downloadAnchor.setAttribute('href', dataStr) - downloadAnchor.setAttribute('download', 'products.json') - document.body.appendChild(downloadAnchor) // required for firefox - downloadAnchor.click() - downloadAnchor.remove() - } - - // function to upload json file and update productMetadataChanges state - const handleUploadJsonButtonClick = () => { - const uploadAnchor = document.createElement('input') - uploadAnchor.setAttribute('type', 'file') - uploadAnchor.setAttribute('accept', '.json') - uploadAnchor.addEventListener('change', (e) => { - const file = (e.target as HTMLInputElement).files![0] - const reader = new FileReader() - reader.onload = (e) => { - if (e.target) { - const fileData = e.target.result - if (!isValidJson(fileData as string)) return - const fileDataParsed = sortData(JSON.parse(fileData as string)) - const changes: Record = {} - Object.keys(fileDataParsed).forEach((symbol) => { - if ( - JSON.stringify(data[symbol]) !== - JSON.stringify(fileDataParsed[symbol]) - ) { - changes[symbol] = { - prev: data[symbol], - new: fileDataParsed[symbol], - } - } - }) - setProductMetadataChanges(changes) - openModal() - } - } - reader.readAsText(file) - }) - document.body.appendChild(uploadAnchor) // required for firefox - uploadAnchor.click() - uploadAnchor.remove() - } - - // check if uploaded json is valid json - const isValidJson = (json: string) => { - try { - JSON.parse(json) - } catch (e: any) { - toast.error(capitalizeFirstLetter(e.message)) - return false - } - // check if json keys are existing products - const jsonParsed = JSON.parse(json) - const jsonSymbols = Object.keys(jsonParsed) - const existingSymbols = Object.keys(data) - // check that jsonSymbols is equal to existingSymbols no matter the order - if ( - JSON.stringify(jsonSymbols.sort()) !== - JSON.stringify(existingSymbols.sort()) - ) { - toast.error('Symbols in json file do not match existing symbols!') - return false - } - - let isValid = true - // check that the keys of the values of json are equal to the keys of the values of data - jsonSymbols.forEach((symbol) => { - const jsonKeys = Object.keys(jsonParsed[symbol]) - const existingKeys = Object.keys(data[symbol]) - if ( - JSON.stringify(jsonKeys.sort()) !== JSON.stringify(existingKeys.sort()) - ) { - toast.error( - `Keys in json file do not match existing keys for symbol ${symbol}!` - ) - isValid = false - } - }) - return isValid - } - - const handleSendProposalButtonClick = async () => { - if (pythProgramClient && productMetadataChanges) { - const instructions: TransactionInstruction[] = [] - Object.keys(productMetadataChanges).forEach((symbol) => { - const { prev, new: newProductMetadata } = productMetadataChanges[symbol] - // prev and new are json object of metadata - // check if there are any new metadata by comparing prev and new values - if (JSON.stringify(prev) !== JSON.stringify(newProductMetadata)) { - pythProgramClient.methods - .updProduct(newProductMetadata) - .accounts({ - fundingAccount: squads?.getAuthorityPDA( - PRICE_FEED_MULTISIG[getMultisigCluster(cluster)], - 1 - ), - productAccount: symbolToProductAccountKeyMapping[symbol], - }) - .instruction() - .then((instruction) => instructions.push(instruction)) - } - }) - - if (!isMultisigLoading && squads) { - setIsSendProposalButtonLoading(true) - try { - const proposalPubkey = await proposeInstructions( - squads, - PRICE_FEED_MULTISIG[getMultisigCluster(cluster)], - instructions, - false - ) - toast.success(`Proposal sent! 🚀 Proposal Pubkey: ${proposalPubkey}`) - setIsSendProposalButtonLoading(false) - } catch (e: any) { - toast.error(capitalizeFirstLetter(e.message)) - setIsSendProposalButtonLoading(false) - } - } - } - } - - const ModalContent = ({ changes }: { changes: any }) => { - return ( - <> - {Object.keys(changes).length > 0 ? ( - - {Object.keys(changes).map((key) => { - const { prev, new: newProductMetadata } = changes[key] - const diff = Object.keys(prev).filter( - (k) => prev[k] !== newProductMetadata[k] - ) - return ( - - - - - {diff.map((k) => ( - - - - - ))} - {/* add a divider only if its not the last item */} - {Object.keys(changes).indexOf(key) !== - Object.keys(changes).length - 1 ? ( - - - - ) : null} - - ) - })} -
- {key} -
- {k - .split('_') - .map((word) => capitalizeFirstLetter(word)) - .join(' ')} - - {prev[k]} -
- {newProductMetadata[k]} -
-
-
- ) : ( -

No proposed changes.

- )} - {Object.keys(changes).length > 0 ? ( - !connected ? ( -
- -
- ) : ( - - ) - ) : null} - - ) - } - - // create anchor wallet when connected - useEffect(() => { - if (connected) { - const provider = new AnchorProvider( - connection, - anchorWallet as Wallet, - AnchorProvider.defaultOptions() - ) - setPythProgramClient( - pythOracleProgram(getPythProgramKeyForCluster(cluster), provider) - ) - } - }, [anchorWallet, connection, connected, cluster]) - - return ( -
- } - /> -
-
-

Update Product Metadata

-
-
-
-
-
- -
-
-
- {dataIsLoading ? ( -
- -
- ) : ( -
-
- -
-
- -
-
- )} -
-
-
- ) -} - -export default UpdateProductMetadata diff --git a/governance/xc_admin/packages/xc_admin_frontend/pages/index.tsx b/governance/xc_admin/packages/xc_admin_frontend/pages/index.tsx index 8834a7bc..a8b4e94e 100644 --- a/governance/xc_admin/packages/xc_admin_frontend/pages/index.tsx +++ b/governance/xc_admin/packages/xc_admin_frontend/pages/index.tsx @@ -3,12 +3,9 @@ import type { NextPage } from 'next' import { useRouter } from 'next/router' import { useEffect, useState } from 'react' import Layout from '../components/layout/Layout' -import AddRemovePublishers from '../components/tabs/AddRemovePublishers' import General from '../components/tabs/General' -import MinPublishers from '../components/tabs/MinPublishers' import Proposals from '../components/tabs/Proposals' import UpdatePermissions from '../components/tabs/UpdatePermissions' -import UpdateProductMetadata from '../components/tabs/UpdateProductMetadata' import { MultisigContextProvider } from '../contexts/MultisigContext' import { PythContextProvider } from '../contexts/PythContext' import { classNames } from '../utils/classNames' @@ -19,27 +16,11 @@ const TAB_INFO = { description: 'General panel for the program.', queryString: 'general', }, - MinPublishers: { - title: 'Min Publishers', - description: - 'Set the minimum number of publishers required to publish a price.', - queryString: 'min-publishers', - }, UpdatePermissions: { title: 'Update Permissions', description: 'Update the permissions of the program.', queryString: 'update-permissions', }, - AddRemovePublishers: { - title: 'Add/Remove Publishers', - description: 'Add or remove publishers from price feeds.', - queryString: 'add-remove-publishers', - }, - UpdateProductMetadata: { - title: 'Update Product Metadata', - description: 'Update the metadata of a product.', - queryString: 'update-product-metadata', - }, Proposals: { title: 'Proposals', description: 'View and vote on proposals.', @@ -114,18 +95,9 @@ const Home: NextPage = () => { {tabInfoArray[currentTabIndex].queryString === TAB_INFO.General.queryString ? ( - ) : tabInfoArray[currentTabIndex].queryString === - TAB_INFO.MinPublishers.queryString ? ( - ) : tabInfoArray[currentTabIndex].queryString === TAB_INFO.UpdatePermissions.queryString ? ( - ) : tabInfoArray[currentTabIndex].queryString === - TAB_INFO.AddRemovePublishers.queryString ? ( - - ) : tabInfoArray[currentTabIndex].queryString === - TAB_INFO.UpdateProductMetadata.queryString ? ( - ) : tabInfoArray[currentTabIndex].queryString === TAB_INFO.Proposals.queryString ? (