[xc-admin] min pub page (#519)

This commit is contained in:
Daniel Chew 2023-01-25 13:50:23 +09:00 committed by GitHub
parent 227079be0e
commit 2c6eb7d1e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 303 additions and 65 deletions

View File

@ -1,20 +1,245 @@
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="mb-4 md:mb-0">
<ClusterSwitch />
<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 ? (
@ -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
</th>
</tr>
{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"
>
<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>
{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>

View File

@ -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
)

View File

@ -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)

View File

@ -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 {