[xc-admin] remove unused tabs (#596)
This commit is contained in:
parent
e3831ec0f6
commit
e841989fa2
|
@ -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<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 && 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<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 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 ? (
|
|
||||||
<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
|
|
|
@ -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<MinPublishersProps>()
|
|
||||||
|
|
||||||
const defaultColumns = [
|
|
||||||
columnHelper.accessor('symbol', {
|
|
||||||
cell: (info) => info.getValue(),
|
|
||||||
header: () => <span>Symbol</span>,
|
|
||||||
}),
|
|
||||||
columnHelper.accessor('minPublishers', {
|
|
||||||
cell: (props) => {
|
|
||||||
const minPublishers = props.getValue()
|
|
||||||
return <span className="mr-2">{minPublishers}</span>
|
|
||||||
},
|
|
||||||
header: () => <span>Min Publishers</span>,
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
|
|
||||||
const MinPublishers = () => {
|
|
||||||
const [data, setData] = useState<MinPublishersProps[]>([])
|
|
||||||
const [columns, setColumns] = useState(() => [...defaultColumns])
|
|
||||||
const [minPublishersChanges, setMinPublishersChanges] =
|
|
||||||
useState<Record<string, MinPublishersInfo>>()
|
|
||||||
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<Program<PythOracle>>()
|
|
||||||
|
|
||||||
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: () => <span>New Min Publishers</span>,
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
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 ? (
|
|
||||||
<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
|
|
||||||
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={minPublishersChanges} />}
|
|
||||||
/>
|
|
||||||
<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">Min 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 className="mb-4 md:mb-0">
|
|
||||||
<EditButton editable={editable} onClick={handleEditButtonClick} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="table-responsive relative mt-6">
|
|
||||||
{dataIsLoading ? (
|
|
||||||
<div className="mt-3">
|
|
||||||
<Loadbar theme="light" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="table-responsive mb-10">
|
|
||||||
<table className="w-full table-auto bg-darkGray text-left">
|
|
||||||
<thead>
|
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
|
||||||
<tr key={headerGroup.id}>
|
|
||||||
{headerGroup.headers.map((header) => (
|
|
||||||
<th
|
|
||||||
key={header.id}
|
|
||||||
className={
|
|
||||||
header.column.id === 'symbol'
|
|
||||||
? 'base16 pt-8 pb-6 pl-4 pr-2 font-semibold opacity-60 xl:pl-14'
|
|
||||||
: 'base16 pt-8 pb-6 pl-1 pr-2 font-semibold opacity-60'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{header.isPlaceholder
|
|
||||||
? null
|
|
||||||
: flexRender(
|
|
||||||
header.column.columnDef.header,
|
|
||||||
header.getContext()
|
|
||||||
)}
|
|
||||||
</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{table.getRowModel().rows.map((row) => (
|
|
||||||
<tr key={row.id} className="border-t border-beige-300">
|
|
||||||
{row.getVisibleCells().map((cell) => (
|
|
||||||
<td
|
|
||||||
key={cell.id}
|
|
||||||
onBlur={(e) =>
|
|
||||||
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()
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MinPublishers
|
|
|
@ -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<string, PublicKey> = {}
|
|
||||||
|
|
||||||
const UpdateProductMetadata = () => {
|
|
||||||
const [data, setData] = useState<SymbolToProductMetadata>({})
|
|
||||||
const [productMetadataChanges, setProductMetadataChanges] =
|
|
||||||
useState<Record<string, ProductMetadataInfo>>()
|
|
||||||
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 && 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<string, ProductMetadataInfo> = {}
|
|
||||||
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 ? (
|
|
||||||
<table className="mb-10 w-full table-auto bg-darkGray text-left">
|
|
||||||
{Object.keys(changes).map((key) => {
|
|
||||||
const { prev, new: newProductMetadata } = changes[key]
|
|
||||||
const diff = Object.keys(prev).filter(
|
|
||||||
(k) => prev[k] !== newProductMetadata[k]
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
<tbody key={key}>
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
className="base16 py-4 pl-6 pr-2 font-bold lg:pl-6"
|
|
||||||
colSpan={2}
|
|
||||||
>
|
|
||||||
{key}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{diff.map((k) => (
|
|
||||||
<tr key={k}>
|
|
||||||
<td className="base16 py-4 pl-6 pr-2 lg:pl-6">
|
|
||||||
{k
|
|
||||||
.split('_')
|
|
||||||
.map((word) => capitalizeFirstLetter(word))
|
|
||||||
.join(' ')}
|
|
||||||
</td>
|
|
||||||
<td className="base16 py-4 pl-1 pr-2 lg:pl-6">
|
|
||||||
<s>{prev[k]}</s>
|
|
||||||
<br />
|
|
||||||
{newProductMetadata[k]}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
{/* add a divider only if its not the last item */}
|
|
||||||
{Object.keys(changes).indexOf(key) !==
|
|
||||||
Object.keys(changes).length - 1 ? (
|
|
||||||
<tr>
|
|
||||||
<td className="base16 py-4 pl-6 pr-6" colSpan={2}>
|
|
||||||
<hr className="border-gray-700" />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
) : null}
|
|
||||||
</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={productMetadataChanges} />}
|
|
||||||
/>
|
|
||||||
<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">Update Product Metadata</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 UpdateProductMetadata
|
|
|
@ -3,12 +3,9 @@ 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 General from '../components/tabs/General'
|
import General from '../components/tabs/General'
|
||||||
import MinPublishers from '../components/tabs/MinPublishers'
|
|
||||||
import Proposals from '../components/tabs/Proposals'
|
import Proposals from '../components/tabs/Proposals'
|
||||||
import UpdatePermissions from '../components/tabs/UpdatePermissions'
|
import UpdatePermissions from '../components/tabs/UpdatePermissions'
|
||||||
import UpdateProductMetadata from '../components/tabs/UpdateProductMetadata'
|
|
||||||
import { MultisigContextProvider } from '../contexts/MultisigContext'
|
import { MultisigContextProvider } from '../contexts/MultisigContext'
|
||||||
import { PythContextProvider } from '../contexts/PythContext'
|
import { PythContextProvider } from '../contexts/PythContext'
|
||||||
import { classNames } from '../utils/classNames'
|
import { classNames } from '../utils/classNames'
|
||||||
|
@ -19,27 +16,11 @@ const TAB_INFO = {
|
||||||
description: 'General panel for the program.',
|
description: 'General panel for the program.',
|
||||||
queryString: 'general',
|
queryString: 'general',
|
||||||
},
|
},
|
||||||
MinPublishers: {
|
|
||||||
title: 'Min Publishers',
|
|
||||||
description:
|
|
||||||
'Set the minimum number of publishers required to publish a price.',
|
|
||||||
queryString: 'min-publishers',
|
|
||||||
},
|
|
||||||
UpdatePermissions: {
|
UpdatePermissions: {
|
||||||
title: 'Update Permissions',
|
title: 'Update Permissions',
|
||||||
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',
|
|
||||||
},
|
|
||||||
UpdateProductMetadata: {
|
|
||||||
title: 'Update Product Metadata',
|
|
||||||
description: 'Update the metadata of a product.',
|
|
||||||
queryString: 'update-product-metadata',
|
|
||||||
},
|
|
||||||
Proposals: {
|
Proposals: {
|
||||||
title: 'Proposals',
|
title: 'Proposals',
|
||||||
description: 'View and vote on proposals.',
|
description: 'View and vote on proposals.',
|
||||||
|
@ -114,18 +95,9 @@ const Home: NextPage = () => {
|
||||||
{tabInfoArray[currentTabIndex].queryString ===
|
{tabInfoArray[currentTabIndex].queryString ===
|
||||||
TAB_INFO.General.queryString ? (
|
TAB_INFO.General.queryString ? (
|
||||||
<General />
|
<General />
|
||||||
) : tabInfoArray[currentTabIndex].queryString ===
|
|
||||||
TAB_INFO.MinPublishers.queryString ? (
|
|
||||||
<MinPublishers />
|
|
||||||
) : tabInfoArray[currentTabIndex].queryString ===
|
) : tabInfoArray[currentTabIndex].queryString ===
|
||||||
TAB_INFO.UpdatePermissions.queryString ? (
|
TAB_INFO.UpdatePermissions.queryString ? (
|
||||||
<UpdatePermissions />
|
<UpdatePermissions />
|
||||||
) : tabInfoArray[currentTabIndex].queryString ===
|
|
||||||
TAB_INFO.AddRemovePublishers.queryString ? (
|
|
||||||
<AddRemovePublishers />
|
|
||||||
) : tabInfoArray[currentTabIndex].queryString ===
|
|
||||||
TAB_INFO.UpdateProductMetadata.queryString ? (
|
|
||||||
<UpdateProductMetadata />
|
|
||||||
) : tabInfoArray[currentTabIndex].queryString ===
|
) : tabInfoArray[currentTabIndex].queryString ===
|
||||||
TAB_INFO.Proposals.queryString ? (
|
TAB_INFO.Proposals.queryString ? (
|
||||||
<Proposals />
|
<Proposals />
|
||||||
|
|
Loading…
Reference in New Issue