diff --git a/governance/xc-admin/packages/xc-admin-frontend/components/tabs/MinPublishers.tsx b/governance/xc-admin/packages/xc-admin-frontend/components/tabs/MinPublishers.tsx index d4e0167f..ff5e5b19 100644 --- a/governance/xc-admin/packages/xc-admin-frontend/components/tabs/MinPublishers.tsx +++ b/governance/xc-admin/packages/xc-admin-frontend/components/tabs/MinPublishers.tsx @@ -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() + +const defaultColumns = [ + columnHelper.accessor('symbol', { + cell: (info) => info.getValue(), + header: () => Symbol, + }), + columnHelper.accessor('minPublishers', { + cell: (props) => { + const minPublishers = props.getValue() + return {minPublishers} + }, + header: () => Min Publishers, + }), +] + const MinPublishers = () => { - const { rawConfig, dataIsLoading } = usePythContext() + const [data, setData] = useState([]) + const [columns, setColumns] = useState(() => [...defaultColumns]) + const [minPublishersChanges, setMinPublishersChanges] = + useState>() + const [editable, setEditable] = useState(false) + const [isModalOpen, setIsModalOpen] = useState(false) + const [isSendProposalButtonLoading, setIsSendProposalButtonLoading] = + useState(false) + const { cluster } = useContext(ClusterContext) + const anchorWallet = useAnchorWallet() + const { isLoading: isMultisigLoading, squads } = useMultisig( + anchorWallet as Wallet + ) + const { rawConfig, dataIsLoading, connection } = usePythContext() + const { connected } = useWallet() + const [pythProgramClient, setPythProgramClient] = + useState>() + + const openModal = () => { + setIsModalOpen(true) + } + + const closeModal = () => { + setIsModalOpen(false) + } + + const handleEditButtonClick = () => { + const nextState = !editable + if (nextState) { + const newColumns = [ + ...defaultColumns, + columnHelper.accessor('newMinPublishers', { + cell: (info) => info.getValue(), + header: () => New Min Publishers, + }), + ] + setColumns(newColumns) + } else { + if ( + minPublishersChanges && + Object.keys(minPublishersChanges).length > 0 + ) { + openModal() + setMinPublishersChanges(minPublishersChanges) + } else { + setColumns(defaultColumns) + } + } + + setEditable(nextState) + } + + const handleEditMinPublishers = ( + e: any, + symbol: string, + prevMinPublishers: number + ) => { + const newMinPublishers = Number(e.target.textContent) + if (prevMinPublishers !== newMinPublishers) { + setMinPublishersChanges({ + ...minPublishersChanges, + [symbol]: { + prev: prevMinPublishers, + new: newMinPublishers, + }, + }) + } else { + // delete symbol from minPublishersChanges if it exists + if (minPublishersChanges && minPublishersChanges[symbol]) { + delete minPublishersChanges[symbol] + } + setMinPublishersChanges(minPublishersChanges) + } + } + + useEffect(() => { + if (!dataIsLoading && rawConfig) { + 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 (
+

Min Publishers

-
- +
+
+ +
+
+ +
{dataIsLoading ? ( @@ -23,50 +248,63 @@ const MinPublishers = () => {
) : (
- +
- - - - + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} - {rawConfig.mappingAccounts.length ? ( - rawConfig.mappingAccounts - .sort( - (mapping1, mapping2) => - mapping2.products.length - mapping1.products.length - )[0] - .products.map((product) => - product.priceAccounts.map((priceAccount) => { - return ( - - - - - ) - }) - ) - ) : ( - - + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} - )} + ))}
- Symbol - - Minimum Publishers -
+ {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} +
- {product.metadata.symbol} - - - {priceAccount.minPub} - -
- No mapping accounts found. -
+ 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() + )} +
diff --git a/governance/xc-admin/packages/xc-admin-frontend/components/tabs/UpdatePermissions.tsx b/governance/xc-admin/packages/xc-admin-frontend/components/tabs/UpdatePermissions.tsx index c416245c..58422398 100644 --- a/governance/xc-admin/packages/xc-admin-frontend/components/tabs/UpdatePermissions.tsx +++ b/governance/xc-admin/packages/xc-admin-frontend/components/tabs/UpdatePermissions.tsx @@ -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 - // 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: () => New Public Key, }), - ]) + ] + 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 ) diff --git a/governance/xc-admin/packages/xc-admin-frontend/hooks/useMultisig.ts b/governance/xc-admin/packages/xc-admin-frontend/hooks/useMultisig.ts index b62b32f8..21814076 100644 --- a/governance/xc-admin/packages/xc-admin-frontend/hooks/useMultisig.ts +++ b/governance/xc-admin/packages/xc-admin-frontend/hooks/useMultisig.ts @@ -19,13 +19,20 @@ export function getMultisigCluster(cluster: PythCluster): Cluster | 'localnet' { } } -export const UPGRADE_MUTLTISIG: Record = { +export const UPGRADE_MULTISIG: Record = { 'mainnet-beta': new PublicKey('FVQyHcooAtThJ83XFrNnv74BcinbRH3bRmfFamAHBfuj'), testnet: new PublicKey('FVQyHcooAtThJ83XFrNnv74BcinbRH3bRmfFamAHBfuj'), devnet: new PublicKey('6baWtW1zTUVMSJHJQVxDUXWzqrQeYBr6mu31j3bTKwY3'), localnet: new PublicKey('FVQyHcooAtThJ83XFrNnv74BcinbRH3bRmfFamAHBfuj'), } +export const SECURITY_MULTISIG: Record = { + '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) diff --git a/governance/xc-admin/packages/xc-admin-frontend/hooks/usePyth.ts b/governance/xc-admin/packages/xc-admin-frontend/hooks/usePyth.ts index 185ee7dc..a1ceaa1d 100644 --- a/governance/xc-admin/packages/xc-admin-frontend/hooks/usePyth.ts +++ b/governance/xc-admin/packages/xc-admin-frontend/hooks/usePyth.ts @@ -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 {