[xc-admin] add add/remove publishers page (#530)
* add initial add/remove publishers page * add download/upload json feature and send proposal * show connect wallet button in change modal when wallet is not connected * address comments * use symbol instead of price account key * sort json * fix sorting bug * remove prettify while sorting * fix upload bug * fix reordering bug
This commit is contained in:
parent
fac47b5bef
commit
e02afb7ee9
|
@ -1,28 +1,18 @@
|
||||||
import { Dialog, Transition } from '@headlessui/react'
|
import { Dialog, Transition } from '@headlessui/react'
|
||||||
import { Dispatch, Fragment, SetStateAction } from 'react'
|
import { Dispatch, Fragment, SetStateAction } from 'react'
|
||||||
import CloseIcon from '../icons/CloseIcon'
|
import CloseIcon from '../icons/CloseIcon'
|
||||||
import Spinner from './Spinner'
|
|
||||||
|
|
||||||
const Modal: React.FC<{
|
const Modal: React.FC<{
|
||||||
isModalOpen: boolean
|
isModalOpen: boolean
|
||||||
setIsModalOpen: Dispatch<SetStateAction<boolean>>
|
setIsModalOpen: Dispatch<SetStateAction<boolean>>
|
||||||
closeModal: () => void
|
closeModal: () => void
|
||||||
changes: any
|
content: any
|
||||||
handleSendProposalButtonClick: () => void
|
}> = ({ isModalOpen, setIsModalOpen, closeModal, content }) => {
|
||||||
isSendProposalButtonLoading: boolean
|
|
||||||
}> = ({
|
|
||||||
isModalOpen,
|
|
||||||
setIsModalOpen,
|
|
||||||
closeModal,
|
|
||||||
changes,
|
|
||||||
handleSendProposalButtonClick,
|
|
||||||
isSendProposalButtonLoading,
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<Transition appear show={isModalOpen} as={Fragment}>
|
<Transition appear show={isModalOpen} as={Fragment}>
|
||||||
<Dialog
|
<Dialog
|
||||||
as="div"
|
as="div"
|
||||||
className="relative z-10"
|
className="relative z-40"
|
||||||
onClose={() => setIsModalOpen(false)}
|
onClose={() => setIsModalOpen(false)}
|
||||||
>
|
>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
|
@ -55,40 +45,7 @@ const Modal: React.FC<{
|
||||||
<Dialog.Title as="h3" className="diaglogTitle">
|
<Dialog.Title as="h3" className="diaglogTitle">
|
||||||
Proposed Changes
|
Proposed Changes
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
|
{content}
|
||||||
{!changes ? (
|
|
||||||
<p className="mb-8 leading-6 ">No proposed changes.</p>
|
|
||||||
) : (
|
|
||||||
Object.keys(changes).map((key) => {
|
|
||||||
if (changes[key].prev !== changes[key].new) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={key}
|
|
||||||
className="flex items-center justify-between pb-4"
|
|
||||||
>
|
|
||||||
<span className="pr-4 text-left font-bold">
|
|
||||||
{key}
|
|
||||||
</span>
|
|
||||||
<span className="mr-2">
|
|
||||||
{changes[key].prev} → {changes[key].new}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="action-btn text-base "
|
|
||||||
onClick={handleSendProposalButtonClick}
|
|
||||||
disabled={!changes}
|
|
||||||
>
|
|
||||||
{isSendProposalButtonLoading ? (
|
|
||||||
<Spinner />
|
|
||||||
) : (
|
|
||||||
'Send Proposal'
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|
|
@ -0,0 +1,395 @@
|
||||||
|
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 { proposeInstructions } from 'xc-admin-common'
|
||||||
|
import { ClusterContext } from '../../contexts/ClusterContext'
|
||||||
|
import { usePythContext } from '../../contexts/PythContext'
|
||||||
|
import {
|
||||||
|
getMultisigCluster,
|
||||||
|
SECURITY_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<string, string> = {}
|
||||||
|
|
||||||
|
const AddRemovePublishers = () => {
|
||||||
|
const [data, setData] = useState<SymbolToPublisherKeys>({})
|
||||||
|
const [publisherChanges, setPublisherChanges] =
|
||||||
|
useState<Record<string, PublishersInfo>>()
|
||||||
|
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<Program<PythOracle>>()
|
||||||
|
|
||||||
|
const openModal = () => {
|
||||||
|
setIsModalOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
setIsModalOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!dataIsLoading && rawConfig) {
|
||||||
|
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<string, PublishersInfo> = {}
|
||||||
|
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 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(
|
||||||
|
SECURITY_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(
|
||||||
|
SECURITY_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,
|
||||||
|
SECURITY_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 ? (
|
||||||
|
<table className="mb-10 w-full table-auto bg-darkGray text-left">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="base16 py-8 pl-6 pr-2 font-semibold lg:pl-6">
|
||||||
|
Description
|
||||||
|
</th>
|
||||||
|
<th className="base16 py-8 pl-1 pr-2 font-semibold lg:pl-6">
|
||||||
|
ID
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
{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 && (
|
||||||
|
<tbody>
|
||||||
|
<Fragment key={key}>
|
||||||
|
<tr>
|
||||||
|
<td className="py-3 pl-6 pr-1 lg:pl-6">Product</td>
|
||||||
|
<td className="py-3 pl-1 pr-8 lg:pl-6">{key}</td>
|
||||||
|
</tr>
|
||||||
|
{publisherKeysToAdd.length > 0 && (
|
||||||
|
<tr>
|
||||||
|
<td className="py-3 pl-6 pr-1 lg:pl-6">
|
||||||
|
Add Publisher(s)
|
||||||
|
</td>
|
||||||
|
<td className="py-3 pl-1 pr-8 lg:pl-6">
|
||||||
|
{publisherKeysToAdd.map((publisherKey: string) => (
|
||||||
|
<span key={publisherKey} className="block">
|
||||||
|
{publisherKey}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{publisherKeysToRemove.length > 0 && (
|
||||||
|
<tr>
|
||||||
|
<td className="py-3 pl-6 pr-1 lg:pl-6">
|
||||||
|
Remove Publisher(s)
|
||||||
|
</td>
|
||||||
|
<td className="py-3 pl-1 pr-8 lg:pl-6">
|
||||||
|
{publisherKeysToRemove.map(
|
||||||
|
(publisherKey: string) => (
|
||||||
|
<span key={publisherKey} className="block">
|
||||||
|
{publisherKey}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
</tbody>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</table>
|
||||||
|
) : (
|
||||||
|
<p className="mb-8 leading-6">No proposed changes.</p>
|
||||||
|
)}
|
||||||
|
{Object.keys(changes).length > 0 ? (
|
||||||
|
!connected ? (
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<WalletModalButton className="action-btn text-base" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className="action-btn text-base"
|
||||||
|
onClick={handleSendProposalButtonClick}
|
||||||
|
>
|
||||||
|
{isSendProposalButtonLoading ? <Spinner /> : 'Send Proposal'}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
) : 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 (
|
||||||
|
<div className="relative">
|
||||||
|
<Modal
|
||||||
|
isModalOpen={isModalOpen}
|
||||||
|
setIsModalOpen={setIsModalOpen}
|
||||||
|
closeModal={closeModal}
|
||||||
|
content={<ModalContent changes={publisherChanges} />}
|
||||||
|
/>
|
||||||
|
<div className="container flex flex-col items-center justify-between lg:flex-row">
|
||||||
|
<div className="mb-4 w-full text-left lg:mb-0">
|
||||||
|
<h1 className="h1 mb-4">Add/Remove Publishers</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="container min-h-[50vh]">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div className="mb-4 md:mb-0">
|
||||||
|
<ClusterSwitch />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="relative mt-6">
|
||||||
|
{dataIsLoading ? (
|
||||||
|
<div className="mt-3">
|
||||||
|
<Loadbar theme="light" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<div className="mb-10">
|
||||||
|
<button
|
||||||
|
className="action-btn text-base"
|
||||||
|
onClick={handleDownloadJsonButtonClick}
|
||||||
|
>
|
||||||
|
Download JSON
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="mb-10">
|
||||||
|
<button
|
||||||
|
className="action-btn text-base"
|
||||||
|
onClick={handleUploadJsonButtonClick}
|
||||||
|
>
|
||||||
|
Upload JSON
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddRemovePublishers
|
|
@ -5,6 +5,7 @@ import {
|
||||||
} from '@pythnetwork/client'
|
} from '@pythnetwork/client'
|
||||||
import { PythOracle } from '@pythnetwork/client/lib/anchor'
|
import { PythOracle } from '@pythnetwork/client/lib/anchor'
|
||||||
import { useAnchorWallet, useWallet } from '@solana/wallet-adapter-react'
|
import { useAnchorWallet, useWallet } from '@solana/wallet-adapter-react'
|
||||||
|
import { WalletModalButton } from '@solana/wallet-adapter-react-ui'
|
||||||
import { TransactionInstruction } from '@solana/web3.js'
|
import { TransactionInstruction } from '@solana/web3.js'
|
||||||
import {
|
import {
|
||||||
createColumnHelper,
|
createColumnHelper,
|
||||||
|
@ -25,6 +26,7 @@ import {
|
||||||
import { capitalizeFirstLetter } from '../../utils/capitalizeFirstLetter'
|
import { capitalizeFirstLetter } from '../../utils/capitalizeFirstLetter'
|
||||||
import ClusterSwitch from '../ClusterSwitch'
|
import ClusterSwitch from '../ClusterSwitch'
|
||||||
import Modal from '../common/Modal'
|
import Modal from '../common/Modal'
|
||||||
|
import Spinner from '../common/Spinner'
|
||||||
import EditButton from '../EditButton'
|
import EditButton from '../EditButton'
|
||||||
import Loadbar from '../loaders/Loadbar'
|
import Loadbar from '../loaders/Loadbar'
|
||||||
|
|
||||||
|
@ -206,6 +208,50 @@ const MinPublishers = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ModalContent = ({ changes }: { changes: any }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{Object.keys(changes).length > 0 ? (
|
||||||
|
<div className="mb-10">
|
||||||
|
{Object.keys(changes).map((key) => {
|
||||||
|
return (
|
||||||
|
changes[key].prev !== changes[key].new && (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
className="mb-4 flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<span className="pr-4 text-left font-bold">{key}</span>
|
||||||
|
<span className="mr-2">
|
||||||
|
{changes[key].prev} → {changes[key].new}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="mb-8 leading-6">No proposed changes.</p>
|
||||||
|
)}
|
||||||
|
{Object.keys(changes).length > 0 ? (
|
||||||
|
!connected ? (
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<WalletModalButton className="action-btn text-base" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className="action-btn text-base"
|
||||||
|
onClick={handleSendProposalButtonClick}
|
||||||
|
>
|
||||||
|
{isSendProposalButtonLoading ? <Spinner /> : 'Send Proposal'}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// create anchor wallet when connected
|
// create anchor wallet when connected
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (connected) {
|
if (connected) {
|
||||||
|
@ -226,16 +272,14 @@ const MinPublishers = () => {
|
||||||
isModalOpen={isModalOpen}
|
isModalOpen={isModalOpen}
|
||||||
setIsModalOpen={setIsModalOpen}
|
setIsModalOpen={setIsModalOpen}
|
||||||
closeModal={closeModal}
|
closeModal={closeModal}
|
||||||
changes={minPublishersChanges}
|
content={<ModalContent changes={minPublishersChanges} />}
|
||||||
handleSendProposalButtonClick={handleSendProposalButtonClick}
|
|
||||||
isSendProposalButtonLoading={isSendProposalButtonLoading}
|
|
||||||
/>
|
/>
|
||||||
<div className="container flex flex-col items-center justify-between lg:flex-row">
|
<div className="container flex flex-col items-center justify-between lg:flex-row">
|
||||||
<div className="mb-4 w-full text-left lg:mb-0">
|
<div className="mb-4 w-full text-left lg:mb-0">
|
||||||
<h1 className="h1 mb-4">Min Publishers</h1>
|
<h1 className="h1 mb-4">Min Publishers</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="container">
|
<div className="container min-h-[50vh]">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<div className="mb-4 md:mb-0">
|
<div className="mb-4 md:mb-0">
|
||||||
<ClusterSwitch />
|
<ClusterSwitch />
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
} from '@pythnetwork/client'
|
} from '@pythnetwork/client'
|
||||||
import { PythOracle } from '@pythnetwork/client/lib/anchor'
|
import { PythOracle } from '@pythnetwork/client/lib/anchor'
|
||||||
import { useAnchorWallet, useWallet } from '@solana/wallet-adapter-react'
|
import { useAnchorWallet, useWallet } from '@solana/wallet-adapter-react'
|
||||||
|
import { WalletModalButton } from '@solana/wallet-adapter-react-ui'
|
||||||
import { PublicKey } from '@solana/web3.js'
|
import { PublicKey } from '@solana/web3.js'
|
||||||
import {
|
import {
|
||||||
createColumnHelper,
|
createColumnHelper,
|
||||||
|
@ -27,6 +28,7 @@ import CopyIcon from '../../images/icons/copy.inline.svg'
|
||||||
import { capitalizeFirstLetter } from '../../utils/capitalizeFirstLetter'
|
import { capitalizeFirstLetter } from '../../utils/capitalizeFirstLetter'
|
||||||
import ClusterSwitch from '../ClusterSwitch'
|
import ClusterSwitch from '../ClusterSwitch'
|
||||||
import Modal from '../common/Modal'
|
import Modal from '../common/Modal'
|
||||||
|
import Spinner from '../common/Spinner'
|
||||||
import EditButton from '../EditButton'
|
import EditButton from '../EditButton'
|
||||||
import Loadbar from '../loaders/Loadbar'
|
import Loadbar from '../loaders/Loadbar'
|
||||||
|
|
||||||
|
@ -282,6 +284,50 @@ const UpdatePermissions = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ModalContent = ({ changes }: { changes: any }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{Object.keys(changes).length > 0 ? (
|
||||||
|
<div className="mb-10">
|
||||||
|
{Object.keys(changes).map((key) => {
|
||||||
|
return (
|
||||||
|
changes[key].prev !== changes[key].new && (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
className="mb-4 flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<span className="pr-4 text-left font-bold">{key}</span>
|
||||||
|
<span className="mr-2">
|
||||||
|
{changes[key].prev} → {changes[key].new}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="mb-8 leading-6">No proposed changes.</p>
|
||||||
|
)}
|
||||||
|
{Object.keys(changes).length > 0 ? (
|
||||||
|
!connected ? (
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<WalletModalButton className="action-btn text-base" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className="action-btn text-base"
|
||||||
|
onClick={handleSendProposalButtonClick}
|
||||||
|
>
|
||||||
|
{isSendProposalButtonLoading ? <Spinner /> : 'Send Proposal'}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// create anchor wallet when connected
|
// create anchor wallet when connected
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (connected) {
|
if (connected) {
|
||||||
|
@ -302,16 +348,14 @@ const UpdatePermissions = () => {
|
||||||
isModalOpen={isModalOpen}
|
isModalOpen={isModalOpen}
|
||||||
setIsModalOpen={setIsModalOpen}
|
setIsModalOpen={setIsModalOpen}
|
||||||
closeModal={closeModal}
|
closeModal={closeModal}
|
||||||
changes={pubkeyChanges}
|
content={<ModalContent changes={pubkeyChanges} />}
|
||||||
handleSendProposalButtonClick={handleSendProposalButtonClick}
|
|
||||||
isSendProposalButtonLoading={isSendProposalButtonLoading}
|
|
||||||
/>
|
/>
|
||||||
<div className="container flex flex-col items-center justify-between lg:flex-row">
|
<div className="container flex flex-col items-center justify-between lg:flex-row">
|
||||||
<div className="mb-4 w-full text-left lg:mb-0">
|
<div className="mb-4 w-full text-left lg:mb-0">
|
||||||
<h1 className="h1 mb-4">Update Permissions</h1>
|
<h1 className="h1 mb-4">Update Permissions</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="container">
|
<div className="container min-h-[50vh]">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<div className="mb-4 md:mb-0">
|
<div className="mb-4 md:mb-0">
|
||||||
<ClusterSwitch />
|
<ClusterSwitch />
|
||||||
|
|
|
@ -3,6 +3,7 @@ import type { NextPage } from 'next'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import Layout from '../components/layout/Layout'
|
import Layout from '../components/layout/Layout'
|
||||||
|
import AddRemovePublishers from '../components/tabs/AddRemovePublishers'
|
||||||
import MinPublishers from '../components/tabs/MinPublishers'
|
import MinPublishers from '../components/tabs/MinPublishers'
|
||||||
import UpdatePermissions from '../components/tabs/UpdatePermissions'
|
import UpdatePermissions from '../components/tabs/UpdatePermissions'
|
||||||
import { PythContextProvider } from '../contexts/PythContext'
|
import { PythContextProvider } from '../contexts/PythContext'
|
||||||
|
@ -20,6 +21,11 @@ const TAB_INFO = {
|
||||||
description: 'Update the permissions of the program.',
|
description: 'Update the permissions of the program.',
|
||||||
queryString: 'update-permissions',
|
queryString: 'update-permissions',
|
||||||
},
|
},
|
||||||
|
AddRemovePublishers: {
|
||||||
|
title: 'Add/Remove Publishers',
|
||||||
|
description: 'Add or remove publishers from price feeds.',
|
||||||
|
queryString: 'add-remove-publishers',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_TAB = 'min-publishers'
|
const DEFAULT_TAB = 'min-publishers'
|
||||||
|
@ -58,13 +64,13 @@ const Home: NextPage = () => {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<PythContextProvider>
|
<PythContextProvider>
|
||||||
<div className="relative pt-16 md:pt-20">
|
<div className="container relative pt-16 md:pt-20">
|
||||||
<div className="py-8 md:py-16">
|
<div className="py-8 md:py-16">
|
||||||
<Tab.Group
|
<Tab.Group
|
||||||
selectedIndex={currentTabIndex}
|
selectedIndex={currentTabIndex}
|
||||||
onChange={handleChangeTab}
|
onChange={handleChangeTab}
|
||||||
>
|
>
|
||||||
<Tab.List className="mx-auto max-w-[526px] gap-1 space-x-4 text-center sm:gap-2.5 md:space-x-8">
|
<Tab.List className="mx-auto gap-1 space-x-4 text-center sm:gap-2.5 md:space-x-8">
|
||||||
{Object.entries(TAB_INFO).map((tab, idx) => (
|
{Object.entries(TAB_INFO).map((tab, idx) => (
|
||||||
<Tab
|
<Tab
|
||||||
key={idx}
|
key={idx}
|
||||||
|
@ -91,6 +97,9 @@ const Home: NextPage = () => {
|
||||||
) : tabInfoArray[currentTabIndex].queryString ===
|
) : tabInfoArray[currentTabIndex].queryString ===
|
||||||
TAB_INFO.UpdatePermissions.queryString ? (
|
TAB_INFO.UpdatePermissions.queryString ? (
|
||||||
<UpdatePermissions />
|
<UpdatePermissions />
|
||||||
|
) : tabInfoArray[currentTabIndex].queryString ===
|
||||||
|
TAB_INFO.AddRemovePublishers.queryString ? (
|
||||||
|
<AddRemovePublishers />
|
||||||
) : null}
|
) : null}
|
||||||
</PythContextProvider>
|
</PythContextProvider>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
Loading…
Reference in New Issue