Przeglądaj źródła

Xc admin frontend refactor (#964)

* Remove fetching proposal internals on page load

Without the internals we can not show the verified/voted icons in the proposals page,
therefore we have to remove them

* Refactor squads creation based on wallet

Previously there was a logic to use a separate wallet for proposeSquads
but now it is removed and we can unify vote/propose squads

* Disable propose button if no action and show hint

* Expose refresh functionality on multisig proposals

* Fetch instructions within proposal and calculate voted/verified properties inside

* Extract WormholeInstructionView as a separate component

Moved some name mappings to pythContext so we don't need prop drilling

* Add support for parsing governance instructions + minor refactors

* Add ability to show / approve upgrade proposals

* fix buttons overflow

* Use the actual targetChainId instead of relying on the cluster context for instruction visualization

* Do not fetch the data again if the multisigCluster remains the same

---------

Co-authored-by: Daniel Chew <cctdaniel@outlook.com>
Mohammad Amin Khashkhashi Moghaddam 2 lat temu
rodzic
commit
f595d61ccd

+ 34 - 0
governance/xc_admin/packages/xc_admin_frontend/components/InstructionViews/AccountUtils.tsx

@@ -0,0 +1,34 @@
+export const SignerTag = () => {
+  return (
+    <div className="flex max-h-[22px] max-w-[74px] items-center justify-center rounded-full bg-[#605D72] py-1 px-2 text-xs">
+      Signer
+    </div>
+  )
+}
+
+export const WritableTag = () => {
+  return (
+    <div className="flex max-h-[22px] max-w-[74px] items-center justify-center rounded-full bg-offPurple py-1 px-2 text-xs">
+      Writable
+    </div>
+  )
+}
+
+export const ParsedAccountPubkeyRow = ({
+  mapping,
+  title,
+  pubkey,
+}: {
+  mapping: { [key: string]: string }
+  title: string
+  pubkey: string
+}) => {
+  return (
+    <div className="flex justify-between pb-3">
+      <div className="max-w-[80px] break-words sm:max-w-none sm:break-normal">
+        &#10551; {title}
+      </div>
+      <div className="space-y-2 sm:flex sm:space-x-2">{mapping[pubkey]}</div>
+    </div>
+  )
+}

+ 451 - 0
governance/xc_admin/packages/xc_admin_frontend/components/InstructionViews/WormholeInstructionView.tsx

@@ -0,0 +1,451 @@
+import {
+  AptosAuthorizeUpgradeContract,
+  AuthorizeGovernanceDataSourceTransfer,
+  CosmosUpgradeContract,
+  EvmSetWormholeAddress,
+  EvmUpgradeContract,
+  ExecutePostedVaa,
+  MessageBufferMultisigInstruction,
+  MultisigParser,
+  PythGovernanceAction,
+  PythMultisigInstruction,
+  RequestGovernanceDataSourceTransfer,
+  SetDataSources,
+  SetFee,
+  SetValidPeriod,
+  UnrecognizedProgram,
+  WormholeMultisigInstruction,
+} from 'xc_admin_common'
+import { AccountMeta, PublicKey } from '@solana/web3.js'
+import CopyPubkey from '../common/CopyPubkey'
+import { useContext } from 'react'
+import { ClusterContext } from '../../contexts/ClusterContext'
+import { ParsedAccountPubkeyRow, SignerTag, WritableTag } from './AccountUtils'
+import { usePythContext } from '../../contexts/PythContext'
+
+import { getMappingCluster, isPubkey } from './utils'
+
+const GovernanceInstructionView = ({
+  instruction,
+  actionName,
+  content,
+}: {
+  instruction: PythGovernanceAction
+  actionName: string
+  content: JSX.Element
+}) => {
+  return (
+    <div className="space-y-4">
+      <div>Action: {actionName}</div>
+      <div>Chain Id: {instruction.targetChainId}</div>
+      {content}
+      <div>
+        Raw payload hex:{' '}
+        <CopyPubkey pubkey={instruction.encode().toString('hex')} />
+      </div>
+    </div>
+  )
+}
+export const WormholeInstructionView = ({
+  instruction,
+}: {
+  instruction: WormholeMultisigInstruction
+}) => {
+  const { cluster } = useContext(ClusterContext)
+  const {
+    priceAccountKeyToSymbolMapping,
+    productAccountKeyToSymbolMapping,
+    publisherKeyToNameMapping,
+  } = usePythContext()
+  const publisherKeyToNameMappingCluster =
+    publisherKeyToNameMapping[getMappingCluster(cluster)]
+  const governanceAction = instruction.governanceAction
+  return (
+    <div className="col-span-4 my-2 space-y-4 bg-darkGray2 p-4 lg:col-span-3">
+      <h4 className="h4">Wormhole Instructions</h4>
+      <hr className="border-[#E6DAFE] opacity-30" />
+      {!governanceAction && (
+        <>
+          <div>Unknown message</div>
+          <div>Raw hex payload:</div>
+          <div>{(instruction.args.payload as Buffer).toString('hex')}</div>
+        </>
+      )}
+      {governanceAction instanceof ExecutePostedVaa &&
+        governanceAction.instructions.map((innerInstruction, index) => {
+          const multisigParser = MultisigParser.fromCluster(cluster)
+          const parsedInstruction = multisigParser.parseInstruction({
+            programId: innerInstruction.programId,
+            data: innerInstruction.data as Buffer,
+            keys: innerInstruction.keys as AccountMeta[],
+          })
+          return (
+            <>
+              <div key={`${index}_program`} className="flex justify-between">
+                <div>Program</div>
+                <div>
+                  {parsedInstruction instanceof PythMultisigInstruction
+                    ? 'Pyth Oracle'
+                    : parsedInstruction instanceof WormholeMultisigInstruction
+                    ? 'Wormhole'
+                    : parsedInstruction instanceof
+                      MessageBufferMultisigInstruction
+                    ? 'Message Buffer'
+                    : 'Unknown'}
+                </div>
+              </div>
+              <div
+                key={`${index}_instructionName`}
+                className="flex justify-between"
+              >
+                <div>Instruction Name</div>
+                <div>
+                  {parsedInstruction instanceof PythMultisigInstruction ||
+                  parsedInstruction instanceof WormholeMultisigInstruction ||
+                  parsedInstruction instanceof MessageBufferMultisigInstruction
+                    ? parsedInstruction.name
+                    : 'Unknown'}
+                </div>
+              </div>
+              <div
+                key={`${index}_arguments`}
+                className="grid grid-cols-4 justify-between"
+              >
+                <div>Arguments</div>
+                {parsedInstruction instanceof PythMultisigInstruction ||
+                parsedInstruction instanceof WormholeMultisigInstruction ||
+                parsedInstruction instanceof
+                  MessageBufferMultisigInstruction ? (
+                  Object.keys(parsedInstruction.args).length > 0 ? (
+                    <div className="col-span-4 mt-2 bg-[#444157] p-4 lg:col-span-3 lg:mt-0">
+                      <div className="base16 flex justify-between pt-2 pb-6 font-semibold opacity-60">
+                        <div>Key</div>
+                        <div>Value</div>
+                      </div>
+                      {Object.keys(parsedInstruction.args).map((key, index) => (
+                        <>
+                          <div
+                            key={index}
+                            className="flex justify-between border-t border-beige-300 py-3"
+                          >
+                            <div>{key}</div>
+                            {parsedInstruction.args[key] instanceof
+                            PublicKey ? (
+                              <CopyPubkey
+                                pubkey={parsedInstruction.args[key].toBase58()}
+                              />
+                            ) : typeof instruction.args[key] === 'string' &&
+                              isPubkey(instruction.args[key]) ? (
+                              <CopyPubkey
+                                pubkey={parsedInstruction.args[key]}
+                              />
+                            ) : (
+                              <div className="max-w-sm break-all">
+                                {typeof parsedInstruction.args[key] === 'string'
+                                  ? parsedInstruction.args[key]
+                                  : parsedInstruction.args[key] instanceof
+                                    Uint8Array
+                                  ? parsedInstruction.args[key].toString('hex')
+                                  : JSON.stringify(parsedInstruction.args[key])}
+                              </div>
+                            )}
+                          </div>
+                          {key === 'pub' &&
+                          parsedInstruction.args[key].toBase58() in
+                            publisherKeyToNameMappingCluster ? (
+                            <ParsedAccountPubkeyRow
+                              key={`${index}_${parsedInstruction.args[
+                                key
+                              ].toBase58()}`}
+                              mapping={publisherKeyToNameMappingCluster}
+                              title="publisher"
+                              pubkey={parsedInstruction.args[key].toBase58()}
+                            />
+                          ) : null}
+                        </>
+                      ))}
+                    </div>
+                  ) : (
+                    <div className="col-span-3 text-right">No arguments</div>
+                  )
+                ) : (
+                  <div className="col-span-3 text-right">Unknown</div>
+                )}
+              </div>
+              {parsedInstruction instanceof PythMultisigInstruction ||
+              parsedInstruction instanceof WormholeMultisigInstruction ||
+              parsedInstruction instanceof MessageBufferMultisigInstruction ? (
+                <div
+                  key={`${index}_accounts`}
+                  className="grid grid-cols-4 justify-between"
+                >
+                  <div>Accounts</div>
+                  {Object.keys(parsedInstruction.accounts.named).length > 0 ? (
+                    <div className="col-span-4 mt-2 bg-[#444157] p-4 lg:col-span-3 lg:mt-0">
+                      <div className="base16 flex justify-between pt-2 pb-6 font-semibold opacity-60">
+                        <div>Account</div>
+                        <div>Pubkey</div>
+                      </div>
+                      {Object.keys(parsedInstruction.accounts.named).map(
+                        (key, index) => (
+                          <>
+                            <div
+                              key={index}
+                              className="flex justify-between border-t border-beige-300 py-3"
+                            >
+                              <div className="max-w-[80px] break-words sm:max-w-none sm:break-normal">
+                                {key}
+                              </div>
+                              <div className="space-y-2 sm:flex sm:space-y-0 sm:space-x-2">
+                                <div className="flex items-center space-x-2 sm:ml-2">
+                                  {parsedInstruction.accounts.named[key]
+                                    .isSigner ? (
+                                    <SignerTag />
+                                  ) : null}
+                                  {parsedInstruction.accounts.named[key]
+                                    .isWritable ? (
+                                    <WritableTag />
+                                  ) : null}
+                                </div>
+                                <CopyPubkey
+                                  pubkey={parsedInstruction.accounts.named[
+                                    key
+                                  ].pubkey.toBase58()}
+                                />
+                              </div>
+                            </div>
+                            {key === 'priceAccount' &&
+                            parsedInstruction.accounts.named[
+                              key
+                            ].pubkey.toBase58() in
+                              priceAccountKeyToSymbolMapping ? (
+                              <ParsedAccountPubkeyRow
+                                key="priceAccountPubkey"
+                                mapping={priceAccountKeyToSymbolMapping}
+                                title="symbol"
+                                pubkey={parsedInstruction.accounts.named[
+                                  key
+                                ].pubkey.toBase58()}
+                              />
+                            ) : key === 'productAccount' &&
+                              parsedInstruction.accounts.named[
+                                key
+                              ].pubkey.toBase58() in
+                                productAccountKeyToSymbolMapping ? (
+                              <ParsedAccountPubkeyRow
+                                key="productAccountPubkey"
+                                mapping={productAccountKeyToSymbolMapping}
+                                title="symbol"
+                                pubkey={parsedInstruction.accounts.named[
+                                  key
+                                ].pubkey.toBase58()}
+                              />
+                            ) : null}
+                          </>
+                        )
+                      )}
+                      {parsedInstruction.accounts.remaining.map(
+                        (accountMeta, index) => (
+                          <>
+                            <div
+                              key="rem-{index}"
+                              className="flex justify-between border-t border-beige-300 py-3"
+                            >
+                              <div className="max-w-[80px] break-words sm:max-w-none sm:break-normal">
+                                Remaining {index + 1}
+                              </div>
+                              <div className="space-y-2 sm:flex sm:space-y-0 sm:space-x-2">
+                                <div className="flex items-center space-x-2 sm:ml-2">
+                                  {accountMeta.isSigner ? <SignerTag /> : null}
+                                  {accountMeta.isWritable ? (
+                                    <WritableTag />
+                                  ) : null}
+                                </div>
+                                <CopyPubkey
+                                  pubkey={accountMeta.pubkey.toBase58()}
+                                />
+                              </div>
+                            </div>
+                          </>
+                        )
+                      )}
+                    </div>
+                  ) : (
+                    <div>No accounts</div>
+                  )}
+                </div>
+              ) : parsedInstruction instanceof UnrecognizedProgram ? (
+                <>
+                  <div
+                    key={`${index}_programId`}
+                    className="flex justify-between"
+                  >
+                    <div>Program ID</div>
+                    <div>
+                      {parsedInstruction.instruction.programId.toBase58()}
+                    </div>
+                  </div>
+                  <div key={`${index}_data`} className="flex justify-between">
+                    <div>Data</div>
+                    <div>
+                      {parsedInstruction.instruction.data.length > 0
+                        ? parsedInstruction.instruction.data.toString('hex')
+                        : 'No data'}
+                    </div>
+                  </div>
+                  <div
+                    key={`${index}_keys`}
+                    className="grid grid-cols-4 justify-between"
+                  >
+                    <div>Keys</div>
+                    <div className="col-span-4 mt-2 bg-darkGray4 p-4 lg:col-span-3 lg:mt-0">
+                      <div className="base16 flex justify-between pt-2 pb-6 font-semibold opacity-60">
+                        <div>Key #</div>
+                        <div>Pubkey</div>
+                      </div>
+                      {parsedInstruction.instruction.keys.map((key, index) => (
+                        <>
+                          <div
+                            key={index}
+                            className="flex justify-between border-t border-beige-300 py-3"
+                          >
+                            <div>Key {index + 1}</div>
+                            <div className="flex space-x-2">
+                              {key.isSigner ? <SignerTag /> : null}
+                              {key.isWritable ? <WritableTag /> : null}
+                              <CopyPubkey pubkey={key.pubkey.toBase58()} />
+                            </div>
+                          </div>
+                        </>
+                      ))}
+                    </div>
+                  </div>
+                </>
+              ) : null}
+            </>
+          )
+        })}
+      {governanceAction instanceof EvmUpgradeContract && (
+        <GovernanceInstructionView
+          instruction={governanceAction}
+          actionName={governanceAction.action}
+          content={
+            <div>
+              Address:
+              <CopyPubkey pubkey={'0x' + governanceAction.address} />
+            </div>
+          }
+        />
+      )}
+
+      {governanceAction instanceof CosmosUpgradeContract && (
+        <GovernanceInstructionView
+          instruction={governanceAction}
+          actionName={governanceAction.action}
+          content={<div>Code id:{governanceAction.codeId.toString()}</div>}
+        />
+      )}
+
+      {governanceAction instanceof AptosAuthorizeUpgradeContract && (
+        <GovernanceInstructionView
+          instruction={governanceAction}
+          actionName={governanceAction.action}
+          content={
+            <div>
+              Package hash:
+              <CopyPubkey pubkey={governanceAction.hash} />
+            </div>
+          }
+        />
+      )}
+
+      {governanceAction instanceof SetFee && (
+        <GovernanceInstructionView
+          instruction={governanceAction}
+          actionName={governanceAction.action}
+          content={
+            <>
+              <div>
+                New Fee Value: {governanceAction.newFeeValue.toString()}
+              </div>
+              <div>New Fee Expo: {governanceAction.newFeeExpo.toString()}</div>
+            </>
+          }
+        />
+      )}
+      {governanceAction instanceof SetDataSources && (
+        <GovernanceInstructionView
+          instruction={governanceAction}
+          actionName={governanceAction.actionName}
+          content={
+            <>
+              {governanceAction.dataSources.map((dataSource, idx) => (
+                <div key={idx}>
+                  Datasource #{idx + 1}:
+                  <ul className="px-4">
+                    <li>Emitter Chain: {dataSource.emitterChain}</li>
+                    <li>
+                      Emitter Address:{' '}
+                      <CopyPubkey pubkey={'0x' + dataSource.emitterAddress} />
+                    </li>
+                  </ul>
+                </div>
+              ))}
+            </>
+          }
+        />
+      )}
+
+      {governanceAction instanceof EvmSetWormholeAddress && (
+        <GovernanceInstructionView
+          instruction={governanceAction}
+          actionName={governanceAction.action}
+          content={
+            <div>
+              New Wormhole Address:
+              <CopyPubkey pubkey={'0x' + governanceAction.address} />
+            </div>
+          }
+        />
+      )}
+
+      {governanceAction instanceof SetValidPeriod && (
+        <GovernanceInstructionView
+          instruction={governanceAction}
+          actionName={governanceAction.action}
+          content={
+            <div>
+              New Valid Period: {governanceAction.newValidPeriod.toString()}
+            </div>
+          }
+        />
+      )}
+
+      {governanceAction instanceof RequestGovernanceDataSourceTransfer && (
+        <GovernanceInstructionView
+          instruction={governanceAction}
+          actionName={governanceAction.action}
+          content={
+            <div>
+              Governance Data Source Index:{' '}
+              {governanceAction.governanceDataSourceIndex}
+            </div>
+          }
+        />
+      )}
+
+      {governanceAction instanceof AuthorizeGovernanceDataSourceTransfer && (
+        <GovernanceInstructionView
+          instruction={governanceAction}
+          actionName={governanceAction.actionName}
+          content={
+            <div>
+              Claim Vaa hex:{' '}
+              <CopyPubkey pubkey={governanceAction.claimVaa.toString('hex')} />
+            </div>
+          }
+        />
+      )}
+    </div>
+  )
+}

+ 19 - 0
governance/xc_admin/packages/xc_admin_frontend/components/InstructionViews/utils.ts

@@ -0,0 +1,19 @@
+import { PublicKey } from '@solana/web3.js'
+
+export const getMappingCluster = (cluster: string) => {
+  if (cluster === 'mainnet-beta' || cluster === 'pythnet') {
+    return 'pythnet'
+  } else {
+    return 'pythtest'
+  }
+}
+
+// check if a string is a pubkey
+export const isPubkey = (str: string) => {
+  try {
+    new PublicKey(str)
+    return true
+  } catch (e) {
+    return false
+  }
+}

+ 12 - 8
governance/xc_admin/packages/xc_admin_frontend/components/tabs/General.tsx

@@ -778,14 +778,18 @@ const General = ({ proposerServerUrl }: { proposerServerUrl: string }) => {
         ) : (
           <p className="mb-8 leading-6">No proposed changes.</p>
         )}
-        {Object.keys(changes).length > 0 ? (
-          <button
-            className="action-btn text-base"
-            onClick={handleSendProposalButtonClick}
-          >
-            {isSendProposalButtonLoading ? <Spinner /> : 'Send Proposal'}
-          </button>
-        ) : null}
+        {Object.keys(changes).length > 0 && (
+          <>
+            <button
+              className="action-btn text-base"
+              onClick={handleSendProposalButtonClick}
+              disabled={isSendProposalButtonLoading || !proposeSquads}
+            >
+              {isSendProposalButtonLoading ? <Spinner /> : 'Send Proposal'}
+            </button>
+            {!proposeSquads && <div>Please connect your wallet</div>}
+          </>
+        )}
       </>
     )
   }

Plik diff jest za duży
+ 230 - 639
governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals.tsx


+ 12 - 20
governance/xc_admin/packages/xc_admin_frontend/contexts/MultisigContext.tsx

@@ -1,25 +1,10 @@
-import { Wallet } from '@coral-xyz/anchor'
 import SquadsMesh from '@sqds/mesh'
 import { MultisigAccount, TransactionAccount } from '@sqds/mesh/lib/types'
 import React, { createContext, useContext, useMemo } from 'react'
 import { MultisigInstruction } from 'xc_admin_common'
-import { useMultisig } from '../hooks/useMultisig'
+import { useMultisig, MultisigHookData } from '../hooks/useMultisig'
 
-// TODO: fix any
-interface MultisigContextProps {
-  isLoading: boolean
-  error: any // TODO: fix any
-  proposeSquads: SquadsMesh | undefined
-  voteSquads: SquadsMesh | undefined
-  upgradeMultisigAccount: MultisigAccount | undefined
-  priceFeedMultisigAccount: MultisigAccount | undefined
-  upgradeMultisigProposals: TransactionAccount[]
-  priceFeedMultisigProposals: TransactionAccount[]
-  allProposalsIxsParsed: MultisigInstruction[][]
-  setpriceFeedMultisigProposals: any
-}
-
-const MultisigContext = createContext<MultisigContextProps>({
+const MultisigContext = createContext<MultisigHookData>({
   upgradeMultisigAccount: undefined,
   priceFeedMultisigAccount: undefined,
   upgradeMultisigProposals: [],
@@ -29,6 +14,8 @@ const MultisigContext = createContext<MultisigContextProps>({
   error: null,
   proposeSquads: undefined,
   voteSquads: undefined,
+  refreshData: undefined,
+  connection: undefined,
   setpriceFeedMultisigProposals: () => {},
 })
 
@@ -36,12 +23,11 @@ export const useMultisigContext = () => useContext(MultisigContext)
 
 interface MultisigContextProviderProps {
   children?: React.ReactNode
-  wallet: Wallet
 }
 
 export const MultisigContextProvider: React.FC<
   MultisigContextProviderProps
-> = ({ children, wallet }) => {
+> = ({ children }) => {
   const {
     isLoading,
     error,
@@ -53,7 +39,9 @@ export const MultisigContextProvider: React.FC<
     priceFeedMultisigProposals,
     allProposalsIxsParsed,
     setpriceFeedMultisigProposals,
-  } = useMultisig(wallet)
+    refreshData,
+    connection,
+  } = useMultisig()
 
   const value = useMemo(
     () => ({
@@ -67,6 +55,8 @@ export const MultisigContextProvider: React.FC<
       error,
       proposeSquads,
       voteSquads,
+      refreshData,
+      connection,
     }),
     [
       proposeSquads,
@@ -79,6 +69,8 @@ export const MultisigContextProvider: React.FC<
       priceFeedMultisigProposals,
       allProposalsIxsParsed,
       setpriceFeedMultisigProposals,
+      refreshData,
+      connection,
     ]
   )
 

+ 54 - 5
governance/xc_admin/packages/xc_admin_frontend/contexts/PythContext.tsx

@@ -1,13 +1,24 @@
-import React, { createContext, useContext, useMemo } from 'react'
+import React, {
+  createContext,
+  useContext,
+  useEffect,
+  useMemo,
+  useState,
+} from 'react'
 import usePyth from '../hooks/usePyth'
 import { RawConfig } from '../hooks/usePyth'
 
 // TODO: fix any
+type AccountKeyToSymbol = { [key: string]: string }
 interface PythContextProps {
   rawConfig: RawConfig
   dataIsLoading: boolean
   error: any
   connection: any
+  priceAccountKeyToSymbolMapping: AccountKeyToSymbol
+  productAccountKeyToSymbolMapping: AccountKeyToSymbol
+  publisherKeyToNameMapping: Record<string, Record<string, string>>
+  multisigSignerKeyToNameMapping: Record<string, string>
 }
 
 const PythContext = createContext<PythContextProps>({
@@ -15,20 +26,47 @@ const PythContext = createContext<PythContextProps>({
   dataIsLoading: true,
   error: null,
   connection: null,
+  priceAccountKeyToSymbolMapping: {},
+  productAccountKeyToSymbolMapping: {},
+  publisherKeyToNameMapping: {},
+  multisigSignerKeyToNameMapping: {},
 })
 
 export const usePythContext = () => useContext(PythContext)
 
 interface PythContextProviderProps {
   children?: React.ReactNode
-  symbols?: string[]
-  raw?: boolean
+  publisherKeyToNameMapping: Record<string, Record<string, string>>
+  multisigSignerKeyToNameMapping: Record<string, string>
 }
-
 export const PythContextProvider: React.FC<PythContextProviderProps> = ({
   children,
+  publisherKeyToNameMapping,
+  multisigSignerKeyToNameMapping,
 }) => {
   const { isLoading, error, connection, rawConfig } = usePyth()
+  const [
+    productAccountKeyToSymbolMapping,
+    setProductAccountKeyToSymbolMapping,
+  ] = useState<AccountKeyToSymbol>({})
+  const [priceAccountKeyToSymbolMapping, setPriceAccountKeyToSymbolMapping] =
+    useState<AccountKeyToSymbol>({})
+
+  useEffect(() => {
+    if (!isLoading) {
+      const productAccountMapping: AccountKeyToSymbol = {}
+      const priceAccountMapping: AccountKeyToSymbol = {}
+      rawConfig.mappingAccounts.map((acc) =>
+        acc.products.map((prod) => {
+          productAccountMapping[prod.address.toBase58()] = prod.metadata.symbol
+          priceAccountMapping[prod.priceAccounts[0].address.toBase58()] =
+            prod.metadata.symbol
+        })
+      )
+      setProductAccountKeyToSymbolMapping(productAccountMapping)
+      setPriceAccountKeyToSymbolMapping(priceAccountMapping)
+    }
+  }, [rawConfig, isLoading])
 
   const value = useMemo(
     () => ({
@@ -36,8 +74,19 @@ export const PythContextProvider: React.FC<PythContextProviderProps> = ({
       dataIsLoading: isLoading,
       error,
       connection,
+      priceAccountKeyToSymbolMapping,
+      productAccountKeyToSymbolMapping,
+      publisherKeyToNameMapping,
+      multisigSignerKeyToNameMapping,
     }),
-    [rawConfig, isLoading, error, connection]
+    [
+      rawConfig,
+      isLoading,
+      error,
+      connection,
+      publisherKeyToNameMapping,
+      multisigSignerKeyToNameMapping,
+    ]
   )
 
   return <PythContext.Provider value={value}>{children}</PythContext.Provider>

+ 47 - 108
governance/xc_admin/packages/xc_admin_frontend/hooks/useMultisig.ts

@@ -1,35 +1,20 @@
-import { Wallet } from '@coral-xyz/anchor'
 import NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet'
-import { getPythProgramKeyForCluster } from '@pythnetwork/client'
 import { useAnchorWallet } from '@solana/wallet-adapter-react'
-import {
-  AccountMeta,
-  Cluster,
-  Connection,
-  Keypair,
-  PublicKey,
-} from '@solana/web3.js'
+import { Connection, Keypair, PublicKey } from '@solana/web3.js'
 import SquadsMesh from '@sqds/mesh'
 import { MultisigAccount, TransactionAccount } from '@sqds/mesh/lib/types'
-import { useContext, useEffect, useState } from 'react'
+import { useCallback, useContext, useEffect, useMemo, useState } from 'react'
 import {
-  ExecutePostedVaa,
-  getManyProposalsInstructions,
   getMultisigCluster,
   getProposals,
-  isRemoteCluster,
   MultisigInstruction,
-  MultisigParser,
   PRICE_FEED_MULTISIG,
-  PythMultisigInstruction,
-  UnrecognizedProgram,
   UPGRADE_MULTISIG,
-  WormholeMultisigInstruction,
 } from 'xc_admin_common'
 import { ClusterContext } from '../contexts/ClusterContext'
 import { pythClusterApiUrls } from '../utils/pythClusterApiUrl'
 
-interface MultisigHookData {
+export interface MultisigHookData {
   isLoading: boolean
   error: any // TODO: fix any
   proposeSquads: SquadsMesh | undefined
@@ -39,6 +24,8 @@ interface MultisigHookData {
   upgradeMultisigProposals: TransactionAccount[]
   priceFeedMultisigProposals: TransactionAccount[]
   allProposalsIxsParsed: MultisigInstruction[][]
+  connection?: Connection
+  refreshData?: () => { fetchData: () => Promise<void>; cancel: () => void }
   setpriceFeedMultisigProposals: React.Dispatch<
     React.SetStateAction<TransactionAccount[]>
   >
@@ -52,7 +39,8 @@ const getSortedProposals = async (
   return proposals.sort((a, b) => b.transactionIndex - a.transactionIndex)
 }
 
-export const useMultisig = (wallet: Wallet): MultisigHookData => {
+export const useMultisig = (): MultisigHookData => {
+  const wallet = useAnchorWallet()
   const { cluster } = useContext(ClusterContext)
   const [isLoading, setIsLoading] = useState(true)
   const [error, setError] = useState(null)
@@ -69,14 +57,11 @@ export const useMultisig = (wallet: Wallet): MultisigHookData => {
   const [allProposalsIxsParsed, setAllProposalsIxsParsed] = useState<
     MultisigInstruction[][]
   >([])
-  const [proposeSquads, setProposeSquads] = useState<SquadsMesh>()
-  const [voteSquads, setVoteSquads] = useState<SquadsMesh>()
-  const anchorWallet = useAnchorWallet()
+  const [squads, setSquads] = useState<SquadsMesh | undefined>()
 
   const [urlsIndex, setUrlsIndex] = useState(0)
 
   useEffect(() => {
-    setIsLoading(true)
     setError(null)
   }, [urlsIndex, cluster])
 
@@ -84,39 +69,34 @@ export const useMultisig = (wallet: Wallet): MultisigHookData => {
     setUrlsIndex(0)
   }, [cluster])
 
-  useEffect(() => {
-    const urls = pythClusterApiUrls(getMultisigCluster(cluster))
-    const connection = new Connection(urls[urlsIndex].rpcUrl, {
+  const multisigCluster = useMemo(() => getMultisigCluster(cluster), [cluster])
+
+  const connection = useMemo(() => {
+    const urls = pythClusterApiUrls(multisigCluster)
+    return new Connection(urls[urlsIndex].rpcUrl, {
       commitment: 'confirmed',
       wsEndpoint: urls[urlsIndex].wsUrl,
     })
+  }, [urlsIndex, multisigCluster])
+
+  useEffect(() => {
     if (wallet) {
-      setProposeSquads(
+      setSquads(
         new SquadsMesh({
           connection,
           wallet,
         })
       )
+    } else {
+      setSquads(undefined)
     }
-    if (anchorWallet) {
-      setVoteSquads(
-        new SquadsMesh({
-          connection,
-          wallet: anchorWallet as Wallet,
-        })
-      )
-    }
-  }, [wallet, urlsIndex, cluster, anchorWallet])
+  }, [wallet, urlsIndex, cluster, connection])
 
-  useEffect(() => {
+  const refreshData = useCallback(() => {
     let cancelled = false
-    const urls = pythClusterApiUrls(getMultisigCluster(cluster))
-    const connection = new Connection(urls[urlsIndex].rpcUrl, {
-      commitment: 'confirmed',
-      wsEndpoint: urls[urlsIndex].wsUrl,
-    })
 
-    ;(async () => {
+    const fetchData = async () => {
+      setIsLoading(true)
       try {
         // mock wallet to allow users to view proposals without connecting their wallet
         const readOnlySquads = new SquadsMesh({
@@ -125,15 +105,13 @@ export const useMultisig = (wallet: Wallet): MultisigHookData => {
         })
         if (cancelled) return
         setUpgradeMultisigAccount(
-          await readOnlySquads.getMultisig(
-            UPGRADE_MULTISIG[getMultisigCluster(cluster)]
-          )
+          await readOnlySquads.getMultisig(UPGRADE_MULTISIG[multisigCluster])
         )
         try {
           if (cancelled) return
           setpriceFeedMultisigAccount(
             await readOnlySquads.getMultisig(
-              PRICE_FEED_MULTISIG[getMultisigCluster(cluster)]
+              PRICE_FEED_MULTISIG[multisigCluster]
             )
           )
         } catch (e) {
@@ -142,67 +120,18 @@ export const useMultisig = (wallet: Wallet): MultisigHookData => {
         }
 
         if (cancelled) return
-        setUpgradeMultisigProposals(
-          await getSortedProposals(
-            readOnlySquads,
-            UPGRADE_MULTISIG[getMultisigCluster(cluster)]
-          )
+        const upgradeProposals = await getSortedProposals(
+          readOnlySquads,
+          UPGRADE_MULTISIG[multisigCluster]
         )
+        setUpgradeMultisigProposals(upgradeProposals)
         try {
           if (cancelled) return
           const sortedPriceFeedMultisigProposals = await getSortedProposals(
             readOnlySquads,
-            PRICE_FEED_MULTISIG[getMultisigCluster(cluster)]
-          )
-          const allProposalsIxs = await getManyProposalsInstructions(
-            readOnlySquads,
-            sortedPriceFeedMultisigProposals
-          )
-          const multisigParser = MultisigParser.fromCluster(
-            getMultisigCluster(cluster)
-          )
-          const parsedAllProposalsIxs = allProposalsIxs.map((ixs) =>
-            ixs.map((ix) =>
-              multisigParser.parseInstruction({
-                programId: ix.programId,
-                data: ix.data as Buffer,
-                keys: ix.keys as AccountMeta[],
-              })
-            )
+            PRICE_FEED_MULTISIG[multisigCluster]
           )
-          const proposalsRes: TransactionAccount[] = []
-          const instructionsRes: MultisigInstruction[][] = []
-          // filter proposals for respective devnet/pythtest and mainnet-beta/pythnet clusters
-          parsedAllProposalsIxs.map((ixs, idx) => {
-            // pythtest/pythnet proposals
-            if (
-              isRemoteCluster(cluster) &&
-              ixs.length > 0 &&
-              ixs.some(
-                (ix) =>
-                  ix instanceof WormholeMultisigInstruction &&
-                  ix.governanceAction instanceof ExecutePostedVaa &&
-                  ix.governanceAction.instructions.some((ix) =>
-                    ix.programId.equals(getPythProgramKeyForCluster(cluster))
-                  )
-              )
-            ) {
-              proposalsRes.push(sortedPriceFeedMultisigProposals[idx])
-              instructionsRes.push(ixs)
-            }
-            // devnet/testnet/mainnet-beta proposals
-            if (
-              !isRemoteCluster(cluster) &&
-              (ixs.length === 0 ||
-                ixs.some((ix) => ix instanceof PythMultisigInstruction) ||
-                ixs.some((ix) => ix instanceof UnrecognizedProgram))
-            ) {
-              proposalsRes.push(sortedPriceFeedMultisigProposals[idx])
-              instructionsRes.push(ixs)
-            }
-          })
-          setAllProposalsIxsParsed(instructionsRes)
-          setpriceFeedMultisigProposals(proposalsRes)
+          setpriceFeedMultisigProposals(sortedPriceFeedMultisigProposals)
         } catch (e) {
           console.error(e)
           setAllProposalsIxsParsed([])
@@ -213,6 +142,7 @@ export const useMultisig = (wallet: Wallet): MultisigHookData => {
       } catch (e) {
         console.log(e)
         if (cancelled) return
+        const urls = pythClusterApiUrls(multisigCluster)
         if (urlsIndex === urls.length - 1) {
           // @ts-ignore
           setError(e)
@@ -225,23 +155,32 @@ export const useMultisig = (wallet: Wallet): MultisigHookData => {
           )
         }
       }
-    })()
-
-    return () => {
+    }
+    const cancel = () => {
       cancelled = true
     }
-  }, [urlsIndex, cluster])
+
+    return { cancel, fetchData }
+  }, [multisigCluster, urlsIndex, connection])
+
+  useEffect(() => {
+    const { cancel, fetchData } = refreshData()
+    fetchData()
+    return cancel
+  }, [refreshData])
 
   return {
     isLoading,
     error,
-    proposeSquads,
-    voteSquads,
+    proposeSquads: squads,
+    voteSquads: squads,
     upgradeMultisigAccount,
     priceFeedMultisigAccount,
     upgradeMultisigProposals,
     priceFeedMultisigProposals,
     allProposalsIxsParsed,
+    refreshData,
+    connection,
     setpriceFeedMultisigProposals,
   }
 }

+ 13 - 19
governance/xc_admin/packages/xc_admin_frontend/pages/index.tsx

@@ -1,8 +1,4 @@
-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 { GetServerSideProps, NextPage } from 'next'
 import { useRouter } from 'next/router'
@@ -88,8 +84,6 @@ const Home: NextPage<{
 }) => {
   const [currentTabIndex, setCurrentTabIndex] = useState(0)
   const tabInfoArray = Object.values(TAB_INFO)
-  const anchorWallet = useAnchorWallet()
-  const wallet = anchorWallet as Wallet
 
   const router = useRouter()
 
@@ -123,8 +117,11 @@ const Home: NextPage<{
 
   return (
     <Layout>
-      <PythContextProvider>
-        <MultisigContextProvider wallet={wallet}>
+      <PythContextProvider
+        publisherKeyToNameMapping={publisherKeyToNameMapping}
+        multisigSignerKeyToNameMapping={multisigSignerKeyToNameMapping}
+      >
+        <MultisigContextProvider>
           <div className="container relative pt-16 md:pt-20">
             <div className="py-8 md:py-16">
               <Tab.Group
@@ -150,20 +147,17 @@ const Home: NextPage<{
             </div>
           </div>
           {tabInfoArray[currentTabIndex].queryString ===
-          TAB_INFO.General.queryString ? (
+            TAB_INFO.General.queryString && (
             <General proposerServerUrl={proposerServerUrl} />
-          ) : tabInfoArray[currentTabIndex].queryString ===
-            TAB_INFO.UpdatePermissions.queryString ? (
-            <UpdatePermissions />
-          ) : tabInfoArray[currentTabIndex].queryString ===
-            TAB_INFO.Proposals.queryString ? (
+          )}
+          {tabInfoArray[currentTabIndex].queryString ===
+            TAB_INFO.UpdatePermissions.queryString && <UpdatePermissions />}
+          {tabInfoArray[currentTabIndex].queryString ===
+            TAB_INFO.Proposals.queryString && (
             <StatusFilterProvider>
-              <Proposals
-                publisherKeyToNameMapping={publisherKeyToNameMapping}
-                multisigSignerKeyToNameMapping={multisigSignerKeyToNameMapping}
-              />
+              <Proposals />
             </StatusFilterProvider>
-          ) : null}
+          )}
         </MultisigContextProvider>
       </PythContextProvider>
     </Layout>

+ 3 - 3
governance/xc_admin/packages/xc_admin_frontend/styles/globals.css

@@ -278,13 +278,13 @@
 }
 
 .dialogTitle {
-  @apply mb-8 text-center font-body text-[32px] leading-[1.1] lg:mb-11 lg:text-[44px] px-10;
+  @apply mb-8 px-10 text-center font-body text-[32px] leading-[1.1] lg:mb-11 lg:text-[44px];
 }
 
 .action-btn {
-  @apply h-[45px] rounded-full bg-pythPurple  px-8 font-mono  font-semibold uppercase  leading-none transition-colors  hover:bg-mediumSlateBlue disabled:opacity-70 disabled:hover:bg-pythPurple;
+  @apply h-[45px] rounded-full bg-pythPurple px-8 font-mono font-semibold uppercase leading-none transition-colors  hover:bg-mediumSlateBlue disabled:opacity-70 disabled:hover:bg-pythPurple;
 }
 
 .sub-action-btn {
-  @apply h-[45px] rounded-full bg-darkGray2  px-8 font-mono  font-semibold uppercase  leading-none transition-colors  hover:bg-darkGray4 disabled:opacity-70 disabled:hover:bg-darkGray2;
+  @apply h-[45px] rounded-full bg-darkGray2 px-8 font-mono font-semibold uppercase leading-none transition-colors hover:bg-darkGray4 disabled:opacity-70 disabled:hover:bg-darkGray2;
 }

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików