[xc-admin] min pub page (#519)
This commit is contained in:
parent
227079be0e
commit
2c6eb7d1e3
|
@ -1,21 +1,246 @@
|
|||
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 { 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 { 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 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 { rawConfig, dataIsLoading } = usePythContext()
|
||||
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) {
|
||||
const minPublishersData: MinPublishersProps[] = []
|
||||
rawConfig.mappingAccounts
|
||||
.sort(
|
||||
(mapping1, mapping2) =>
|
||||
mapping2.products.length - mapping1.products.length
|
||||
)[0]
|
||||
.products.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(
|
||||
SECURITY_MULTISIG[getMultisigCluster(cluster)],
|
||||
1
|
||||
),
|
||||
})
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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}
|
||||
changes={minPublishersChanges}
|
||||
handleSendProposalButtonClick={handleSendProposalButtonClick}
|
||||
isSendProposalButtonLoading={isSendProposalButtonLoading}
|
||||
/>
|
||||
<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">
|
||||
<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">
|
||||
|
@ -23,50 +248,63 @@ const MinPublishers = () => {
|
|||
</div>
|
||||
) : (
|
||||
<div className="table-responsive mb-10">
|
||||
<table className="w-full bg-darkGray text-left">
|
||||
<table className="w-full table-auto bg-darkGray text-left">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="base16 pt-8 pb-6 pl-4 pr-2 font-semibold opacity-60 lg:pl-14">
|
||||
Symbol
|
||||
</th>
|
||||
<th className="base16 pt-8 pb-6 pl-1 pr-2 font-semibold opacity-60 lg:pl-14">
|
||||
Minimum Publishers
|
||||
{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>
|
||||
{rawConfig.mappingAccounts.length ? (
|
||||
rawConfig.mappingAccounts
|
||||
.sort(
|
||||
(mapping1, mapping2) =>
|
||||
mapping2.products.length - mapping1.products.length
|
||||
)[0]
|
||||
.products.map((product) =>
|
||||
product.priceAccounts.map((priceAccount) => {
|
||||
return (
|
||||
<tr
|
||||
key={product.metadata.symbol}
|
||||
className="border-t border-beige-300"
|
||||
{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'
|
||||
}
|
||||
>
|
||||
<td className="py-3 pl-4 pr-2 lg:pl-14">
|
||||
{product.metadata.symbol}
|
||||
</td>
|
||||
<td className="py-3 pl-1 lg:pl-14">
|
||||
<span className="mr-2">
|
||||
{priceAccount.minPub}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
)
|
||||
) : (
|
||||
<tr className="border-t border-beige-300">
|
||||
<td className="py-3 pl-4 lg:pl-14" colSpan={2}>
|
||||
No mapping accounts found.
|
||||
</td>
|
||||
</tr>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
|
@ -20,7 +20,7 @@ import { ClusterContext } from '../../contexts/ClusterContext'
|
|||
import { usePythContext } from '../../contexts/PythContext'
|
||||
import {
|
||||
getMultisigCluster,
|
||||
UPGRADE_MUTLTISIG,
|
||||
UPGRADE_MULTISIG,
|
||||
useMultisig,
|
||||
} from '../../hooks/useMultisig'
|
||||
import CopyIcon from '../../images/icons/copy.inline.svg'
|
||||
|
@ -86,7 +86,6 @@ const defaultColumns = [
|
|||
}),
|
||||
]
|
||||
|
||||
// make a type with 3 possible values
|
||||
type PermissionAccount =
|
||||
| 'Master Authority'
|
||||
| 'Data Curation Authority'
|
||||
|
@ -141,7 +140,7 @@ const UpdatePermissions = () => {
|
|||
},
|
||||
])
|
||||
}
|
||||
}, [dataIsLoading, rawConfig])
|
||||
}, [rawConfig])
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
|
@ -175,28 +174,17 @@ const UpdatePermissions = () => {
|
|||
return newPubkeyChanges
|
||||
}
|
||||
|
||||
// let newPubkeyChanges: Record<PermissionAccount, PermissionAccountInfo>
|
||||
// data.forEach((d) => {
|
||||
// if (!newPubkeyChanges[d.account]) {
|
||||
// newPubkeyChanges[d.account] = {
|
||||
// prev: d.pubkey,
|
||||
// new: d.pubkey,
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
|
||||
// return newPubkeyChanges
|
||||
|
||||
const handleEditButtonClick = () => {
|
||||
const nextState = !editable
|
||||
if (nextState) {
|
||||
setColumns([
|
||||
const newColumns = [
|
||||
...defaultColumns,
|
||||
columnHelper.accessor('newPubkey', {
|
||||
cell: (info) => info.getValue(),
|
||||
header: () => <span>New Public Key</span>,
|
||||
}),
|
||||
])
|
||||
]
|
||||
setColumns(newColumns)
|
||||
} else {
|
||||
if (pubkeyChanges && Object.keys(pubkeyChanges).length > 0) {
|
||||
openModal()
|
||||
|
@ -257,7 +245,7 @@ const UpdatePermissions = () => {
|
|||
)
|
||||
.accounts({
|
||||
upgradeAuthority: squads?.getAuthorityPDA(
|
||||
UPGRADE_MUTLTISIG[getMultisigCluster(cluster)],
|
||||
UPGRADE_MULTISIG[getMultisigCluster(cluster)],
|
||||
1
|
||||
),
|
||||
programDataAccount,
|
||||
|
@ -269,7 +257,7 @@ const UpdatePermissions = () => {
|
|||
try {
|
||||
const proposalPubkey = await proposeInstructions(
|
||||
squads,
|
||||
UPGRADE_MUTLTISIG[getMultisigCluster(cluster)],
|
||||
UPGRADE_MULTISIG[getMultisigCluster(cluster)],
|
||||
[instruction],
|
||||
false
|
||||
)
|
||||
|
|
|
@ -19,13 +19,20 @@ export function getMultisigCluster(cluster: PythCluster): Cluster | 'localnet' {
|
|||
}
|
||||
}
|
||||
|
||||
export const UPGRADE_MUTLTISIG: Record<Cluster | 'localnet', PublicKey> = {
|
||||
export const UPGRADE_MULTISIG: Record<Cluster | 'localnet', PublicKey> = {
|
||||
'mainnet-beta': new PublicKey('FVQyHcooAtThJ83XFrNnv74BcinbRH3bRmfFamAHBfuj'),
|
||||
testnet: new PublicKey('FVQyHcooAtThJ83XFrNnv74BcinbRH3bRmfFamAHBfuj'),
|
||||
devnet: new PublicKey('6baWtW1zTUVMSJHJQVxDUXWzqrQeYBr6mu31j3bTKwY3'),
|
||||
localnet: new PublicKey('FVQyHcooAtThJ83XFrNnv74BcinbRH3bRmfFamAHBfuj'),
|
||||
}
|
||||
|
||||
export const SECURITY_MULTISIG: Record<Cluster | 'localnet', PublicKey> = {
|
||||
'mainnet-beta': new PublicKey('92hQkq8kBgCUcF9yWN8URZB9RTmA4mZpDGtbiAWA74Z8'), // TODO: placeholder value for now, fix when vault is created
|
||||
testnet: new PublicKey('92hQkq8kBgCUcF9yWN8URZB9RTmA4mZpDGtbiAWA74Z8'), // TODO: placeholder value for now, fix when vault is created
|
||||
devnet: new PublicKey('92hQkq8kBgCUcF9yWN8URZB9RTmA4mZpDGtbiAWA74Z8'),
|
||||
localnet: new PublicKey('92hQkq8kBgCUcF9yWN8URZB9RTmA4mZpDGtbiAWA74Z8'), // TODO: placeholder value for now, fix when vault is created
|
||||
}
|
||||
|
||||
interface MultisigHookData {
|
||||
isLoading: boolean
|
||||
error: any // TODO: fix any
|
||||
|
@ -66,7 +73,7 @@ export const useMultisig = (wallet: Wallet): MultisigHookData => {
|
|||
setProposals(
|
||||
await getProposals(
|
||||
squads,
|
||||
UPGRADE_MUTLTISIG[getMultisigCluster(cluster)]
|
||||
UPGRADE_MULTISIG[getMultisigCluster(cluster)]
|
||||
)
|
||||
)
|
||||
setSquads(squads)
|
||||
|
|
|
@ -73,6 +73,7 @@ const usePyth = (): PythHookData => {
|
|||
const allPythAccounts = await connection.getProgramAccounts(
|
||||
getPythProgramKeyForCluster(cluster)
|
||||
)
|
||||
if (cancelled) return
|
||||
const priceRawConfigs: { [key: string]: PriceRawConfig } = {}
|
||||
|
||||
/// First pass, price accounts
|
||||
|
@ -99,6 +100,7 @@ const usePyth = (): PythHookData => {
|
|||
}
|
||||
}
|
||||
|
||||
if (cancelled) return
|
||||
/// Second pass, product accounts
|
||||
i = 0
|
||||
const productRawConfigs: { [key: string]: ProductRawConfig } = {}
|
||||
|
@ -139,6 +141,7 @@ const usePyth = (): PythHookData => {
|
|||
}
|
||||
|
||||
const rawConfig: RawConfig = { mappingAccounts: [] }
|
||||
if (cancelled) return
|
||||
/// Third pass, mapping accounts
|
||||
i = 0
|
||||
while (i < allPythAccounts.length) {
|
||||
|
@ -193,7 +196,9 @@ const usePyth = (): PythHookData => {
|
|||
}
|
||||
})()
|
||||
|
||||
return () => {}
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [urlsIndex, cluster])
|
||||
|
||||
return {
|
||||
|
|
Loading…
Reference in New Issue