Przeglądaj źródła

[xc-admin] improve xc-admin-frontend UI/UX (#673)

* add expired status

* add proposal status filter

* allow proposals to be proposed from ops key

* check if ops keypair file exists

* update mappings

* fix UpdatePermissions

* trigger deployment

* fix precommit

* address comments

* use SECRETS_BASE_PATH

* fix path

* fix
Daniel Chew 2 lat temu
rodzic
commit
af77a2a987

+ 1 - 1
governance/xc_admin/packages/xc_admin_frontend/.gitignore

@@ -37,5 +37,5 @@ next-env.d.ts
 
 
 # mappings
-publishers.json
+publishers-*.json
 signers.json

+ 88 - 0
governance/xc_admin/packages/xc_admin_frontend/components/ProposalStatusFilter.tsx

@@ -0,0 +1,88 @@
+import { Menu, Transition } from '@headlessui/react'
+import { useRouter } from 'next/router'
+import { Fragment, useCallback, useContext, useEffect } from 'react'
+import {
+  DEFAULT_STATUS_FILTER,
+  StatusFilterContext,
+} from '../contexts/StatusFilterContext'
+import Arrow from '../images/icons/down.inline.svg'
+
+const ProposalStatusFilter = () => {
+  const router = useRouter()
+  const { statusFilter, setStatusFilter } = useContext(StatusFilterContext)
+
+  const handleChange = useCallback(
+    (event: any) => {
+      if (event.target.value) {
+        router.query.status = event.target.value
+        setStatusFilter(event.target.value)
+        router.push(
+          {
+            pathname: router.pathname,
+            query: router.query,
+          },
+          undefined,
+          { scroll: false }
+        )
+      }
+    },
+    [setStatusFilter, router]
+  )
+
+  useEffect(() => {
+    router.query && router.query.status
+      ? setStatusFilter(router.query.status as string)
+      : setStatusFilter(DEFAULT_STATUS_FILTER)
+  }, [setStatusFilter, router])
+
+  const statuses = [
+    'all',
+    'active',
+    'executed',
+    'executeReady',
+    'cancelled',
+    'rejected',
+    'draft',
+    'expired',
+  ]
+
+  return (
+    <Menu as="div" className="relative z-[3] block w-[180px] text-left">
+      {({ open }) => (
+        <>
+          <Menu.Button
+            className={`inline-flex w-full items-center justify-between bg-darkGray2 py-3 px-6 text-sm outline-0`}
+          >
+            <span className="mr-3">{statusFilter}</span>
+            <Arrow className={`${open && 'rotate-180'}`} />
+          </Menu.Button>
+          <Transition
+            as={Fragment}
+            enter="transition ease-out duration-100"
+            enterFrom="transform opacity-0 scale-95"
+            enterTo="transform opacity-100 scale-100"
+            leave="transition ease-in duration-75"
+            leaveFrom="transform opacity-100 scale-100"
+            leaveTo="transform opacity-0 scale-95"
+          >
+            <Menu.Items className="absolute right-0 mt-2 w-full origin-top-right">
+              {statuses.map((s) => (
+                <Menu.Item key={s}>
+                  <button
+                    className={`block w-full bg-darkGray py-3 px-6 text-left text-sm hover:bg-darkGray2`}
+                    value={s}
+                    onClick={handleChange}
+                  >
+                    {s}
+                  </button>
+                </Menu.Item>
+              ))}
+            </Menu.Items>
+          </Transition>
+        </>
+      )}
+    </Menu>
+  )
+}
+
+export default ProposalStatusFilter

+ 18 - 16
governance/xc_admin/packages/xc_admin_frontend/components/tabs/General.tsx

@@ -1,7 +1,7 @@
-import { AnchorProvider, Program, Wallet } from '@coral-xyz/anchor'
+import { AnchorProvider, Program } from '@coral-xyz/anchor'
 import { AccountType, getPythProgramKeyForCluster } from '@pythnetwork/client'
 import { PythOracle, pythOracleProgram } from '@pythnetwork/client/lib/anchor'
-import { useAnchorWallet, useWallet } from '@solana/wallet-adapter-react'
+import { useWallet } from '@solana/wallet-adapter-react'
 import { WalletModalButton } from '@solana/wallet-adapter-react-ui'
 import { Cluster, PublicKey, TransactionInstruction } from '@solana/web3.js'
 import { useCallback, useContext, useEffect, useState } from 'react'
@@ -15,8 +15,9 @@ import {
   WORMHOLE_ADDRESS,
 } from 'xc_admin_common'
 import { ClusterContext } from '../../contexts/ClusterContext'
+import { useMultisigContext } from '../../contexts/MultisigContext'
 import { usePythContext } from '../../contexts/PythContext'
-import { PRICE_FEED_MULTISIG, useMultisig } from '../../hooks/useMultisig'
+import { PRICE_FEED_MULTISIG } from '../../hooks/useMultisig'
 import { capitalizeFirstLetter } from '../../utils/capitalizeFirstLetter'
 import ClusterSwitch from '../ClusterSwitch'
 import Modal from '../common/Modal'
@@ -35,11 +36,7 @@ const General = () => {
   const isRemote: boolean = isRemoteCluster(cluster) // Move to multisig context
   const multisigCluster: Cluster | 'localnet' = getMultisigCluster(cluster) // Move to multisig context
   const wormholeAddress = WORMHOLE_ADDRESS[multisigCluster] // Move to multisig context
-
-  const anchorWallet = useAnchorWallet()
-  const { isLoading: isMultisigLoading, squads } = useMultisig(
-    anchorWallet as Wallet
-  )
+  const { isLoading: isMultisigLoading, proposeSquads } = useMultisigContext()
   const { rawConfig, dataIsLoading, connection } = usePythContext()
   const { connected } = useWallet()
   const [pythProgramClient, setPythProgramClient] =
@@ -268,10 +265,15 @@ const General = () => {
   }
 
   const handleSendProposalButtonClick = async () => {
-    if (pythProgramClient && dataChanges && !isMultisigLoading && squads) {
+    if (
+      pythProgramClient &&
+      dataChanges &&
+      !isMultisigLoading &&
+      proposeSquads
+    ) {
       const instructions: TransactionInstruction[] = []
       for (const symbol of Object.keys(dataChanges)) {
-        const multisigAuthority = squads.getAuthorityPDA(
+        const multisigAuthority = proposeSquads.getAuthorityPDA(
           PRICE_FEED_MULTISIG[getMultisigCluster(cluster)],
           1
         )
@@ -447,7 +449,7 @@ const General = () => {
       setIsSendProposalButtonLoading(true)
       try {
         const proposalPubkey = await proposeInstructions(
-          squads,
+          proposeSquads,
           PRICE_FEED_MULTISIG[getMultisigCluster(cluster)],
           instructions,
           isRemote,
@@ -714,17 +716,17 @@ const General = () => {
 
   // create anchor wallet when connected
   useEffect(() => {
-    if (connected) {
+    if (connected && proposeSquads) {
       const provider = new AnchorProvider(
         connection,
-        anchorWallet as Wallet,
+        proposeSquads.wallet,
         AnchorProvider.defaultOptions()
       )
       setPythProgramClient(
         pythOracleProgram(getPythProgramKeyForCluster(cluster), provider)
       )
     }
-  }, [anchorWallet, connection, connected, cluster])
+  }, [connection, connected, cluster, proposeSquads])
 
   return (
     <div className="relative">
@@ -749,12 +751,12 @@ const General = () => {
           <PermissionDepermissionKey
             isPermission={true}
             pythProgramClient={pythProgramClient}
-            squads={squads}
+            squads={proposeSquads}
           />
           <PermissionDepermissionKey
             isPermission={false}
             pythProgramClient={pythProgramClient}
-            squads={squads}
+            squads={proposeSquads}
           />
         </div>
         <div className="relative mt-6">

+ 94 - 37
governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals.tsx

@@ -25,6 +25,7 @@ import {
 import { ClusterContext } from '../../contexts/ClusterContext'
 import { useMultisigContext } from '../../contexts/MultisigContext'
 import { usePythContext } from '../../contexts/PythContext'
+import { StatusFilterContext } from '../../contexts/StatusFilterContext'
 import { PRICE_FEED_MULTISIG } from '../../hooks/useMultisig'
 import VerifiedIcon from '../../images/icons/verified.inline.svg'
 import { capitalizeFirstLetter } from '../../utils/capitalizeFirstLetter'
@@ -32,6 +33,7 @@ import ClusterSwitch from '../ClusterSwitch'
 import CopyPubkey from '../common/CopyPubkey'
 import Spinner from '../common/Spinner'
 import Loadbar from '../loaders/Loadbar'
+import ProposalStatusFilter from '../ProposalStatusFilter'
 
 // check if a string is a pubkey
 const isPubkey = (str: string) => {
@@ -43,16 +45,26 @@ const isPubkey = (str: string) => {
   }
 }
 
+const getMappingCluster = (cluster: string) => {
+  if (cluster === 'mainnet-beta' || cluster === 'pythnet') {
+    return 'pythnet'
+  } else {
+    return 'pythtest'
+  }
+}
+
 const ProposalRow = ({
   proposal,
   verified,
   setCurrentProposalPubkey,
+  multisig,
 }: {
   proposal: TransactionAccount
   verified: boolean
   setCurrentProposalPubkey: Dispatch<SetStateAction<string | undefined>>
+  multisig: MultisigAccount | undefined
 }) => {
-  const status = Object.keys(proposal.status)[0]
+  const status = getProposalStatus(proposal, multisig)
 
   const router = useRouter()
 
@@ -126,6 +138,8 @@ const StatusTag = ({ proposalStatus }: { proposalStatus: string }) => {
           ? 'bg-[#C4428F]'
           : proposalStatus === 'rejected'
           ? 'bg-[#CF6E42]'
+          : proposalStatus === 'expired'
+          ? 'bg-[#A52A2A]'
           : 'bg-pythPurple'
       } py-1 px-2 text-xs`}
     >
@@ -172,6 +186,21 @@ const ParsedAccountPubkeyRow = ({
   )
 }
 
+const getProposalStatus = (
+  proposal: TransactionAccount | undefined,
+  multisig: MultisigAccount | undefined
+): string => {
+  if (multisig && proposal) {
+    const onChainStatus = Object.keys(proposal.status)[0]
+    return proposal.transactionIndex <= multisig.msChangeIndex &&
+      (onChainStatus == 'active' || onChainStatus == 'draft')
+      ? 'expired'
+      : onChainStatus
+  } else {
+    return 'unkwown'
+  }
+}
+
 const Proposal = ({
   publisherKeyToNameMapping,
   multisigSignerKeyToNameMapping,
@@ -181,7 +210,7 @@ const Proposal = ({
   verified,
   multisig,
 }: {
-  publisherKeyToNameMapping: Record<string, string>
+  publisherKeyToNameMapping: Record<string, Record<string, string>>
   multisigSignerKeyToNameMapping: Record<string, string>
   proposal: TransactionAccount | undefined
   proposalIndex: number
@@ -198,12 +227,14 @@ const Proposal = ({
   const [priceAccountKeyToSymbolMapping, setPriceAccountKeyToSymbolMapping] =
     useState<{ [key: string]: string }>({})
   const { cluster } = useContext(ClusterContext)
+  const publisherKeyToNameMappingCluster =
+    publisherKeyToNameMapping[getMappingCluster(cluster)]
   const {
-    squads,
+    voteSquads,
     isLoading: isMultisigLoading,
     setpriceFeedMultisigProposals,
   } = useMultisigContext()
-  const { rawConfig, dataIsLoading, connection } = usePythContext()
+  const { rawConfig, dataIsLoading } = usePythContext()
 
   useEffect(() => {
     setCurrentProposal(proposal)
@@ -225,7 +256,7 @@ const Proposal = ({
     }
   }, [rawConfig, dataIsLoading])
 
-  const proposalStatus = proposal ? Object.keys(proposal.status)[0] : 'unknown'
+  const proposalStatus = getProposalStatus(proposal, multisig)
 
   useEffect(() => {
     // update the priceFeedMultisigProposals with previous value but replace the current proposal with the updated one at the specific index
@@ -238,12 +269,12 @@ const Proposal = ({
   }, [currentProposal, setpriceFeedMultisigProposals, proposalIndex])
 
   const handleClickApprove = async () => {
-    if (proposal && squads) {
+    if (proposal && voteSquads) {
       try {
         setIsTransactionLoading(true)
-        await squads.approveTransaction(proposal.publicKey)
+        await voteSquads.approveTransaction(proposal.publicKey)
         const proposals = await getProposals(
-          squads,
+          voteSquads,
           PRICE_FEED_MULTISIG[getMultisigCluster(cluster)]
         )
         setCurrentProposal(
@@ -263,12 +294,12 @@ const Proposal = ({
   }
 
   const handleClickReject = async () => {
-    if (proposal && squads) {
+    if (proposal && voteSquads) {
       try {
         setIsTransactionLoading(true)
-        await squads.rejectTransaction(proposal.publicKey)
+        await voteSquads.rejectTransaction(proposal.publicKey)
         const proposals = await getProposals(
-          squads,
+          voteSquads,
           PRICE_FEED_MULTISIG[getMultisigCluster(cluster)]
         )
         setCurrentProposal(
@@ -288,12 +319,12 @@ const Proposal = ({
   }
 
   const handleClickExecute = async () => {
-    if (proposal && squads) {
+    if (proposal && voteSquads) {
       try {
         setIsTransactionLoading(true)
-        await squads.executeTransaction(proposal.publicKey)
+        await voteSquads.executeTransaction(proposal.publicKey)
         const proposals = await getProposals(
-          squads,
+          voteSquads,
           PRICE_FEED_MULTISIG[getMultisigCluster(cluster)]
         )
         setCurrentProposal(
@@ -313,12 +344,12 @@ const Proposal = ({
   }
 
   const handleClickCancel = async () => {
-    if (proposal && squads) {
+    if (proposal && voteSquads) {
       try {
         setIsTransactionLoading(true)
-        await squads.cancelTransaction(proposal.publicKey)
+        await voteSquads.cancelTransaction(proposal.publicKey)
         const proposals = await getProposals(
-          squads,
+          voteSquads,
           PRICE_FEED_MULTISIG[getMultisigCluster(cluster)]
         )
         setCurrentProposal(
@@ -574,12 +605,12 @@ const Proposal = ({
                           </div>
                           {key === 'pub' &&
                           instruction.args[key].toBase58() in
-                            publisherKeyToNameMapping ? (
+                            publisherKeyToNameMappingCluster ? (
                             <ParsedAccountPubkeyRow
                               key={`${index}_${instruction.args[
                                 key
                               ].toBase58()}`}
-                              mapping={publisherKeyToNameMapping}
+                              mapping={publisherKeyToNameMappingCluster}
                               title="publisher"
                               pubkey={instruction.args[key].toBase58()}
                             />
@@ -825,13 +856,13 @@ const Proposal = ({
                                           parsedInstruction.args[
                                             key
                                           ].toBase58() in
-                                            publisherKeyToNameMapping ? (
+                                            publisherKeyToNameMappingCluster ? (
                                             <ParsedAccountPubkeyRow
                                               key={`${index}_${parsedInstruction.args[
                                                 key
                                               ].toBase58()}`}
                                               mapping={
-                                                publisherKeyToNameMapping
+                                                publisherKeyToNameMappingCluster
                                               }
                                               title="publisher"
                                               pubkey={parsedInstruction.args[
@@ -1028,7 +1059,7 @@ const Proposals = ({
   publisherKeyToNameMapping,
   multisigSignerKeyToNameMapping,
 }: {
-  publisherKeyToNameMapping: Record<string, string>
+  publisherKeyToNameMapping: Record<string, Record<string, string>>
   multisigSignerKeyToNameMapping: Record<string, string>
 }) => {
   const router = useRouter()
@@ -1039,12 +1070,16 @@ const Proposals = ({
   >([])
   const [currentProposalPubkey, setCurrentProposalPubkey] = useState<string>()
   const { cluster } = useContext(ClusterContext)
+  const { statusFilter } = useContext(StatusFilterContext)
   const {
     priceFeedMultisigAccount,
     priceFeedMultisigProposals,
     allProposalsIxsParsed,
     isLoading: isMultisigLoading,
   } = useMultisigContext()
+  const [filteredProposals, setFilteredProposals] = useState<
+    TransactionAccount[]
+  >(priceFeedMultisigProposals)
 
   useEffect(() => {
     if (!isMultisigLoading) {
@@ -1125,6 +1160,21 @@ const Proposals = ({
     cluster,
   ])
 
+  useEffect(() => {
+    // filter price feed multisig proposals by status
+    if (statusFilter === 'all') {
+      setFilteredProposals(priceFeedMultisigProposals)
+    } else {
+      setFilteredProposals(
+        priceFeedMultisigProposals.filter(
+          (proposal) =>
+            getProposalStatus(proposal, priceFeedMultisigAccount) ===
+            statusFilter
+        )
+      )
+    }
+  }, [statusFilter, priceFeedMultisigAccount, priceFeedMultisigProposals])
+
   return (
     <div className="relative">
       <div className="container flex flex-col items-center justify-between lg:flex-row">
@@ -1147,26 +1197,33 @@ const Proposals = ({
                 <div className="mt-3">
                   <Loadbar theme="light" />
                 </div>
-              ) : priceFeedMultisigProposals.length > 0 ? (
+              ) : (
                 <>
-                  <div className="pb-4">
+                  <div className="flex items-center justify-between pb-4">
                     <h4 className="h4">
-                      Total Proposals: {priceFeedMultisigProposals.length}
+                      Total Proposals: {filteredProposals.length}
                     </h4>
+                    <ProposalStatusFilter />
                   </div>
-                  <div className="flex flex-col">
-                    {priceFeedMultisigProposals.map((proposal, idx) => (
-                      <ProposalRow
-                        key={idx}
-                        proposal={proposal}
-                        verified={allProposalsVerifiedArr[idx]}
-                        setCurrentProposalPubkey={setCurrentProposalPubkey}
-                      />
-                    ))}
-                  </div>
+                  {filteredProposals.length > 0 ? (
+                    <div className="flex flex-col">
+                      {filteredProposals.map((proposal, idx) => (
+                        <ProposalRow
+                          key={idx}
+                          proposal={proposal}
+                          verified={allProposalsVerifiedArr[idx]}
+                          setCurrentProposalPubkey={setCurrentProposalPubkey}
+                          multisig={priceFeedMultisigAccount}
+                        />
+                      ))}
+                    </div>
+                  ) : (
+                    <div className="mt-4">
+                      No proposals found. If you&apos;re a member of the price
+                      feed multisig, you can create a proposal.
+                    </div>
+                  )}
                 </>
-              ) : (
-                "No proposals found. If you're a member of the price feed multisig, you can create a proposal."
               )}
             </div>
           </>

+ 10 - 12
governance/xc_admin/packages/xc_admin_frontend/components/tabs/UpdatePermissions.tsx

@@ -1,4 +1,4 @@
-import { AnchorProvider, Program, Wallet } from '@coral-xyz/anchor'
+import { AnchorProvider, Program } from '@coral-xyz/anchor'
 import {
   getPythProgramKeyForCluster,
   pythOracleProgram,
@@ -25,8 +25,9 @@ import {
   WORMHOLE_ADDRESS,
 } from 'xc_admin_common'
 import { ClusterContext } from '../../contexts/ClusterContext'
+import { useMultisigContext } from '../../contexts/MultisigContext'
 import { usePythContext } from '../../contexts/PythContext'
-import { UPGRADE_MULTISIG, useMultisig } from '../../hooks/useMultisig'
+import { UPGRADE_MULTISIG } from '../../hooks/useMultisig'
 import CopyIcon from '../../images/icons/copy.inline.svg'
 import { capitalizeFirstLetter } from '../../utils/capitalizeFirstLetter'
 import ClusterSwitch from '../ClusterSwitch'
@@ -109,10 +110,7 @@ const UpdatePermissions = () => {
   const [isSendProposalButtonLoading, setIsSendProposalButtonLoading] =
     useState(false)
   const { cluster } = useContext(ClusterContext)
-  const anchorWallet = useAnchorWallet()
-  const { isLoading: isMultisigLoading, squads } = useMultisig(
-    anchorWallet as Wallet
-  )
+  const { isLoading: isMultisigLoading, proposeSquads } = useMultisigContext()
   const { rawConfig, dataIsLoading, connection } = usePythContext()
   const { connected } = useWallet()
   const [pythProgramClient, setPythProgramClient] =
@@ -241,12 +239,12 @@ const UpdatePermissions = () => {
   }
 
   const handleSendProposalButtonClick = () => {
-    if (pythProgramClient && finalPubkeyChanges && squads) {
+    if (pythProgramClient && finalPubkeyChanges && proposeSquads) {
       const programDataAccount = PublicKey.findProgramAddressSync(
         [pythProgramClient?.programId.toBuffer()],
         BPF_UPGRADABLE_LOADER
       )[0]
-      const multisigAuthority = squads.getAuthorityPDA(
+      const multisigAuthority = proposeSquads.getAuthorityPDA(
         UPGRADE_MULTISIG[getMultisigCluster(cluster)],
         1
       )
@@ -269,7 +267,7 @@ const UpdatePermissions = () => {
             setIsSendProposalButtonLoading(true)
             try {
               const proposalPubkey = await proposeInstructions(
-                squads,
+                proposeSquads,
                 UPGRADE_MULTISIG[getMultisigCluster(cluster)],
                 [instruction],
                 isRemoteCluster(cluster),
@@ -334,17 +332,17 @@ const UpdatePermissions = () => {
 
   // create anchor wallet when connected
   useEffect(() => {
-    if (connected) {
+    if (connected && proposeSquads) {
       const provider = new AnchorProvider(
         connection,
-        anchorWallet as Wallet,
+        proposeSquads.wallet,
         AnchorProvider.defaultOptions()
       )
       setPythProgramClient(
         pythOracleProgram(getPythProgramKeyForCluster(cluster), provider)
       )
     }
-  }, [anchorWallet, connection, connected, cluster])
+  }, [connection, connected, cluster, proposeSquads])
 
   return (
     <div className="relative">

+ 13 - 9
governance/xc_admin/packages/xc_admin_frontend/contexts/MultisigContext.tsx

@@ -1,5 +1,4 @@
 import { Wallet } from '@coral-xyz/anchor'
-import { useAnchorWallet } from '@solana/wallet-adapter-react'
 import SquadsMesh from '@sqds/mesh'
 import { MultisigAccount, TransactionAccount } from '@sqds/mesh/lib/types'
 import React, { createContext, useContext, useMemo } from 'react'
@@ -10,7 +9,8 @@ import { useMultisig } from '../hooks/useMultisig'
 interface MultisigContextProps {
   isLoading: boolean
   error: any // TODO: fix any
-  squads: SquadsMesh | undefined
+  proposeSquads: SquadsMesh | undefined
+  voteSquads: SquadsMesh | undefined
   upgradeMultisigAccount: MultisigAccount | undefined
   priceFeedMultisigAccount: MultisigAccount | undefined
   upgradeMultisigProposals: TransactionAccount[]
@@ -27,7 +27,8 @@ const MultisigContext = createContext<MultisigContextProps>({
   allProposalsIxsParsed: [],
   isLoading: true,
   error: null,
-  squads: undefined,
+  proposeSquads: undefined,
+  voteSquads: undefined,
   setpriceFeedMultisigProposals: () => {},
 })
 
@@ -35,23 +36,24 @@ export const useMultisigContext = () => useContext(MultisigContext)
 
 interface MultisigContextProviderProps {
   children?: React.ReactNode
+  wallet: Wallet
 }
 
 export const MultisigContextProvider: React.FC<
   MultisigContextProviderProps
-> = ({ children }) => {
-  const anchorWallet = useAnchorWallet()
+> = ({ children, wallet }) => {
   const {
     isLoading,
     error,
-    squads,
+    proposeSquads,
+    voteSquads,
     upgradeMultisigAccount,
     priceFeedMultisigAccount,
     upgradeMultisigProposals,
     priceFeedMultisigProposals,
     allProposalsIxsParsed,
     setpriceFeedMultisigProposals,
-  } = useMultisig(anchorWallet as Wallet)
+  } = useMultisig(wallet)
 
   const value = useMemo(
     () => ({
@@ -63,10 +65,12 @@ export const MultisigContextProvider: React.FC<
       setpriceFeedMultisigProposals,
       isLoading,
       error,
-      squads,
+      proposeSquads,
+      voteSquads,
     }),
     [
-      squads,
+      proposeSquads,
+      voteSquads,
       isLoading,
       error,
       upgradeMultisigAccount,

+ 27 - 0
governance/xc_admin/packages/xc_admin_frontend/contexts/StatusFilterContext.tsx

@@ -0,0 +1,27 @@
+import { createContext, useMemo, useState } from 'react'
+
+export const DEFAULT_STATUS_FILTER = 'all'
+
+export const StatusFilterContext = createContext<{
+  statusFilter: string
+  setStatusFilter: any
+}>({
+  statusFilter: DEFAULT_STATUS_FILTER,
+  setStatusFilter: {},
+})
+
+export const StatusFilterProvider = (props: any) => {
+  const [statusFilter, setStatusFilter] = useState<string>(
+    DEFAULT_STATUS_FILTER
+  )
+  const contextValue = useMemo(
+    () => ({
+      statusFilter,
+      setStatusFilter: (statusFilter: string) => {
+        setStatusFilter(statusFilter)
+      },
+    }),
+    [statusFilter]
+  )
+  return <StatusFilterContext.Provider {...props} value={contextValue} />
+}

+ 20 - 7
governance/xc_admin/packages/xc_admin_frontend/hooks/useMultisig.ts

@@ -1,16 +1,16 @@
 import { Wallet } from '@coral-xyz/anchor'
 import NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet'
+import { useAnchorWallet } from '@solana/wallet-adapter-react'
 import {
   AccountMeta,
   Cluster,
   Connection,
   Keypair,
   PublicKey,
-  Transaction,
 } from '@solana/web3.js'
 import SquadsMesh from '@sqds/mesh'
 import { MultisigAccount, TransactionAccount } from '@sqds/mesh/lib/types'
-import { useContext, useEffect, useRef, useState } from 'react'
+import { useContext, useEffect, useState } from 'react'
 import {
   getManyProposalsInstructions,
   getMultisigCluster,
@@ -42,7 +42,8 @@ export const PRICE_FEED_MULTISIG: Record<Cluster | 'localnet', PublicKey> = {
 interface MultisigHookData {
   isLoading: boolean
   error: any // TODO: fix any
-  squads: SquadsMesh | undefined
+  proposeSquads: SquadsMesh | undefined
+  voteSquads: SquadsMesh | undefined
   upgradeMultisigAccount: MultisigAccount | undefined
   priceFeedMultisigAccount: MultisigAccount | undefined
   upgradeMultisigProposals: TransactionAccount[]
@@ -78,7 +79,10 @@ export const useMultisig = (wallet: Wallet): MultisigHookData => {
   const [allProposalsIxsParsed, setAllProposalsIxsParsed] = useState<
     MultisigInstruction[][]
   >([])
-  const [squads, setSquads] = useState<SquadsMesh>()
+  const [proposeSquads, setProposeSquads] = useState<SquadsMesh>()
+  const [voteSquads, setVoteSquads] = useState<SquadsMesh>()
+  const anchorWallet = useAnchorWallet()
+
   const [urlsIndex, setUrlsIndex] = useState(0)
 
   useEffect(() => {
@@ -93,14 +97,22 @@ export const useMultisig = (wallet: Wallet): MultisigHookData => {
       wsEndpoint: urls[urlsIndex].wsUrl,
     })
     if (wallet) {
-      setSquads(
+      setProposeSquads(
         new SquadsMesh({
           connection,
           wallet,
         })
       )
     }
-  }, [wallet, urlsIndex, cluster])
+    if (anchorWallet) {
+      setVoteSquads(
+        new SquadsMesh({
+          connection,
+          wallet: anchorWallet as Wallet,
+        })
+      )
+    }
+  }, [wallet, urlsIndex, cluster, anchorWallet])
 
   useEffect(() => {
     let cancelled = false
@@ -224,7 +236,8 @@ export const useMultisig = (wallet: Wallet): MultisigHookData => {
   return {
     isLoading,
     error,
-    squads,
+    proposeSquads,
+    voteSquads,
     upgradeMultisigAccount,
     priceFeedMultisigAccount,
     upgradeMultisigProposals,

+ 56 - 22
governance/xc_admin/packages/xc_admin_frontend/pages/index.tsx

@@ -1,6 +1,10 @@
+import { Wallet } from '@coral-xyz/anchor'
+import NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet'
 import { Tab } from '@headlessui/react'
+import { useAnchorWallet } from '@solana/wallet-adapter-react'
+import { Keypair } from '@solana/web3.js'
 import * as fs from 'fs'
-import type { GetStaticProps, NextPage } from 'next'
+import type { GetServerSideProps, NextPage } from 'next'
 import { useRouter } from 'next/router'
 import { useEffect, useState } from 'react'
 import Layout from '../components/layout/Layout'
@@ -9,30 +13,47 @@ import Proposals from '../components/tabs/Proposals'
 import UpdatePermissions from '../components/tabs/UpdatePermissions'
 import { MultisigContextProvider } from '../contexts/MultisigContext'
 import { PythContextProvider } from '../contexts/PythContext'
+import { StatusFilterProvider } from '../contexts/StatusFilterContext'
 import { classNames } from '../utils/classNames'
 
-export const getStaticProps: GetStaticProps = async () => {
-  const publisherMappingFilePath = `${
-    process.env.MAPPING_BASE_PATH || ''
-  }publishers.json`
-  const publisherKeyToNameMapping = fs.existsSync(publisherMappingFilePath)
-    ? JSON.parse(
-        (await fs.promises.readFile(publisherMappingFilePath)).toString()
-      )
-    : {}
-  const multisigSignerMappingFilePath = `${
-    process.env.MAPPING_BASE_PATH || ''
-  }signers.json`
+export const getServerSideProps: GetServerSideProps = async () => {
+  const KEYPAIR_BASE_PATH = process.env.KEYPAIR_BASE_PATH || ''
+  const OPS_WALLET = fs.existsSync(KEYPAIR_BASE_PATH)
+    ? JSON.parse(fs.readFileSync(KEYPAIR_BASE_PATH, 'ascii'))
+    : null
+
+  const MAPPINGS_BASE_PATH = process.env.MAPPINGS_BASE_PATH || ''
+  const PUBLISHER_PYTHNET_MAPPING_PATH = `${MAPPINGS_BASE_PATH}/publishers-pythnet.json`
+  const PUBLISHER_PYTHTEST_MAPPING_PATH = `${MAPPINGS_BASE_PATH}/publishers-pythtest.json`
+
+  const publisherKeyToNameMapping = {
+    pythnet: fs.existsSync(PUBLISHER_PYTHNET_MAPPING_PATH)
+      ? JSON.parse(
+          (
+            await fs.promises.readFile(PUBLISHER_PYTHNET_MAPPING_PATH)
+          ).toString()
+        )
+      : {},
+    pythtest: fs.existsSync(PUBLISHER_PYTHTEST_MAPPING_PATH)
+      ? JSON.parse(
+          (
+            await fs.promises.readFile(PUBLISHER_PYTHTEST_MAPPING_PATH)
+          ).toString()
+        )
+      : {},
+  }
+  const MULTISIG_SIGNER_MAPPING_PATH = `${MAPPINGS_BASE_PATH}/signers.json`
   const multisigSignerKeyToNameMapping = fs.existsSync(
-    multisigSignerMappingFilePath
+    MULTISIG_SIGNER_MAPPING_PATH
   )
     ? JSON.parse(
-        (await fs.promises.readFile(multisigSignerMappingFilePath)).toString()
+        (await fs.promises.readFile(MULTISIG_SIGNER_MAPPING_PATH)).toString()
       )
     : {}
 
   return {
     props: {
+      OPS_WALLET,
       publisherKeyToNameMapping,
       multisigSignerKeyToNameMapping,
     },
@@ -60,11 +81,22 @@ const TAB_INFO = {
 const DEFAULT_TAB = 'general'
 
 const Home: NextPage<{
-  publisherKeyToNameMapping: Record<string, string>
+  OPS_WALLET: number[] | null
+  publisherKeyToNameMapping: Record<string, Record<string, string>>
   multisigSignerKeyToNameMapping: Record<string, string>
-}> = ({ publisherKeyToNameMapping, multisigSignerKeyToNameMapping }) => {
+}> = ({
+  OPS_WALLET,
+  publisherKeyToNameMapping,
+  multisigSignerKeyToNameMapping,
+}) => {
   const [currentTabIndex, setCurrentTabIndex] = useState(0)
   const tabInfoArray = Object.values(TAB_INFO)
+  const anchorWallet = useAnchorWallet()
+  const wallet = OPS_WALLET
+    ? (new NodeWallet(
+        Keypair.fromSecretKey(Uint8Array.from(OPS_WALLET))
+      ) as Wallet)
+    : (anchorWallet as Wallet)
 
   const router = useRouter()
 
@@ -99,7 +131,7 @@ const Home: NextPage<{
   return (
     <Layout>
       <PythContextProvider>
-        <MultisigContextProvider>
+        <MultisigContextProvider wallet={wallet}>
           <div className="container relative pt-16 md:pt-20">
             <div className="py-8 md:py-16">
               <Tab.Group
@@ -132,10 +164,12 @@ const Home: NextPage<{
             <UpdatePermissions />
           ) : tabInfoArray[currentTabIndex].queryString ===
             TAB_INFO.Proposals.queryString ? (
-            <Proposals
-              publisherKeyToNameMapping={publisherKeyToNameMapping}
-              multisigSignerKeyToNameMapping={multisigSignerKeyToNameMapping}
-            />
+            <StatusFilterProvider>
+              <Proposals
+                publisherKeyToNameMapping={publisherKeyToNameMapping}
+                multisigSignerKeyToNameMapping={multisigSignerKeyToNameMapping}
+              />
+            </StatusFilterProvider>
           ) : null}
         </MultisigContextProvider>
       </PythContextProvider>