Browse Source

feat(xc-admin-frontend): instructions summary in proposal page + improve ui in proposal row + refactor the code (#1478)

* refactor: move proposals to a folder

* refactor: use @images instead of relative paths

* refactor: split proposals into multiple files

* refactor: add type for proposal status

* refactor: add eslint and fix errors

* refactor: fix eslint errors

* refactor: fix eslint

* refactor: fix prettier

* refactor: remove any

* refactor: Proposals.tsx

* feat: add basic instructions summary

* feat: add unknown instruction

* fix: revert package-lock.json

* fix: update package-lock.json

* fix: pre-commit

* fix: ts error

* fix: remove message buffer dependency

* fix: revert back the cluster default

* feat: add support for different types of instructions

* feat: add transaction index to proposal row

* feat: improve the proposal row ui

* fix: display bigint properly (#1499)

---------

Co-authored-by: guibescos <59208140+guibescos@users.noreply.github.com>
Keyvan Khademi 1 year ago
parent
commit
b110bbca5c
29 changed files with 1232 additions and 659 deletions
  1. 2 0
      governance/xc_admin/packages/xc_admin_frontend/.eslintignore
  2. 12 1
      governance/xc_admin/packages/xc_admin_frontend/.eslintrc.json
  3. 4 3
      governance/xc_admin/packages/xc_admin_frontend/components/ClusterSwitch.tsx
  4. 2 5
      governance/xc_admin/packages/xc_admin_frontend/components/PermissionDepermissionKey.tsx
  5. 4 3
      governance/xc_admin/packages/xc_admin_frontend/components/ProposalStatusFilter.tsx
  6. 1 1
      governance/xc_admin/packages/xc_admin_frontend/components/common/CopyText.tsx
  7. 6 6
      governance/xc_admin/packages/xc_admin_frontend/components/common/SocialLinks.tsx
  8. 2 2
      governance/xc_admin/packages/xc_admin_frontend/components/layout/Header.tsx
  9. 1 1
      governance/xc_admin/packages/xc_admin_frontend/components/layout/Layout.tsx
  10. 3 4
      governance/xc_admin/packages/xc_admin_frontend/components/layout/MobileMenu.tsx
  11. 5 8
      governance/xc_admin/packages/xc_admin_frontend/components/tabs/General.tsx
  12. 25 0
      governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals/InstructionsSummary.tsx
  13. 42 357
      governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals/Proposal.tsx
  14. 191 0
      governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals/ProposalRow.tsx
  15. 218 0
      governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals/Proposals.tsx
  16. 37 0
      governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals/StatusTag.tsx
  17. 100 0
      governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals/utils.ts
  18. 2 3
      governance/xc_admin/packages/xc_admin_frontend/components/tabs/UpdatePermissions.tsx
  19. 9 5
      governance/xc_admin/packages/xc_admin_frontend/contexts/ClusterContext.tsx
  20. 1 12
      governance/xc_admin/packages/xc_admin_frontend/contexts/MultisigContext.tsx
  21. 4 9
      governance/xc_admin/packages/xc_admin_frontend/contexts/PythContext.tsx
  22. 15 8
      governance/xc_admin/packages/xc_admin_frontend/contexts/StatusFilterContext.tsx
  23. 2 17
      governance/xc_admin/packages/xc_admin_frontend/hooks/useMultisig.ts
  24. 2 12
      governance/xc_admin/packages/xc_admin_frontend/hooks/usePyth.ts
  25. 7 0
      governance/xc_admin/packages/xc_admin_frontend/next.config.js
  26. 2 1
      governance/xc_admin/packages/xc_admin_frontend/package.json
  27. 1 1
      governance/xc_admin/packages/xc_admin_frontend/pages/index.tsx
  28. 5 1
      governance/xc_admin/packages/xc_admin_frontend/tsconfig.json
  29. 527 199
      package-lock.json

+ 2 - 0
governance/xc_admin/packages/xc_admin_frontend/.eslintignore

@@ -1 +1,3 @@
 node_modules
+tailwind.config.js
+next.config.js

+ 12 - 1
governance/xc_admin/packages/xc_admin_frontend/.eslintrc.json

@@ -1,3 +1,14 @@
 {
-  "extends": "next/core-web-vitals"
+  "extends": [
+    "plugin:@typescript-eslint/recommended",
+    "next/core-web-vitals",
+    "prettier"
+  ],
+  "rules": {
+    "@typescript-eslint/no-explicit-any": "warn",
+    "@typescript-eslint/no-unused-vars": [
+      "error",
+      { "argsIgnorePattern": "^_" }
+    ]
+  }
 }

+ 4 - 3
governance/xc_admin/packages/xc_admin_frontend/components/ClusterSwitch.tsx

@@ -2,7 +2,8 @@ import { Menu, Transition } from '@headlessui/react'
 import { useRouter } from 'next/router'
 import { Fragment, useCallback, useContext, useEffect } from 'react'
 import { ClusterContext, DEFAULT_CLUSTER } from '../contexts/ClusterContext'
-import Arrow from '../images/icons/down.inline.svg'
+import Arrow from '@images/icons/down.inline.svg'
+import { PythCluster } from '@pythnetwork/client'
 
 const ClusterSwitch = ({ light }: { light?: boolean | null }) => {
   const router = useRouter()
@@ -27,8 +28,8 @@ const ClusterSwitch = ({ light }: { light?: boolean | null }) => {
   )
 
   useEffect(() => {
-    router.query && router.query.cluster
-      ? setCluster(router.query.cluster)
+    router?.query?.cluster
+      ? setCluster(router.query.cluster as PythCluster)
       : setCluster(DEFAULT_CLUSTER)
   }, [setCluster, router])
 

+ 2 - 5
governance/xc_admin/packages/xc_admin_frontend/components/PermissionDepermissionKey.tsx

@@ -4,7 +4,7 @@ import { PythOracle } from '@pythnetwork/client/lib/anchor'
 import * as Label from '@radix-ui/react-label'
 import { useWallet } from '@solana/wallet-adapter-react'
 import { WalletModalButton } from '@solana/wallet-adapter-react-ui'
-import { Cluster, PublicKey, TransactionInstruction } from '@solana/web3.js'
+import { PublicKey, TransactionInstruction } from '@solana/web3.js'
 import SquadsMesh from '@sqds/mesh'
 import axios from 'axios'
 import { Fragment, useContext, useEffect, useState } from 'react'
@@ -15,12 +15,11 @@ import {
   isRemoteCluster,
   mapKey,
   PRICE_FEED_MULTISIG,
-  WORMHOLE_ADDRESS,
 } from 'xc_admin_common'
 import { ClusterContext } from '../contexts/ClusterContext'
 import { usePythContext } from '../contexts/PythContext'
 import { ProductRawConfig } from '../hooks/usePyth'
-import Arrow from '../images/icons/down.inline.svg'
+import Arrow from '@images/icons/down.inline.svg'
 import { capitalizeFirstLetter } from '../utils/capitalizeFirstLetter'
 import Spinner from './common/Spinner'
 import CloseIcon from './icons/CloseIcon'
@@ -83,8 +82,6 @@ const PermissionDepermissionKey = ({
         1
       )
       const isRemote: boolean = isRemoteCluster(cluster)
-      const multisigCluster: Cluster | 'localnet' = getMultisigCluster(cluster)
-      const wormholeAddress = WORMHOLE_ADDRESS[multisigCluster]
       const fundingAccount = isRemote
         ? mapKey(multisigAuthority)
         : multisigAuthority

+ 4 - 3
governance/xc_admin/packages/xc_admin_frontend/components/ProposalStatusFilter.tsx

@@ -3,9 +3,10 @@ import { useRouter } from 'next/router'
 import { Fragment, useCallback, useContext, useEffect } from 'react'
 import {
   DEFAULT_STATUS_FILTER,
+  type ProposalStatusFilter,
   StatusFilterContext,
 } from '../contexts/StatusFilterContext'
-import Arrow from '../images/icons/down.inline.svg'
+import Arrow from '@images/icons/down.inline.svg'
 
 const ProposalStatusFilter = () => {
   const router = useRouter()
@@ -31,11 +32,11 @@ const ProposalStatusFilter = () => {
 
   useEffect(() => {
     router.query && router.query.status
-      ? setStatusFilter(router.query.status as string)
+      ? setStatusFilter(router.query.status as ProposalStatusFilter)
       : setStatusFilter(DEFAULT_STATUS_FILTER)
   }, [setStatusFilter, router])
 
-  const statuses = [
+  const statuses: ProposalStatusFilter[] = [
     'all',
     'active',
     'executed',

+ 1 - 1
governance/xc_admin/packages/xc_admin_frontend/components/common/CopyText.tsx

@@ -1,5 +1,5 @@
 import copy from 'copy-to-clipboard'
-import CopyIcon from '../../images/icons/copy.inline.svg'
+import CopyIcon from '@images/icons/copy.inline.svg'
 
 const CopyText: React.FC<{
   text: string

+ 6 - 6
governance/xc_admin/packages/xc_admin_frontend/components/common/SocialLinks.tsx

@@ -1,10 +1,10 @@
 import Link from 'next/link'
-import Discord from '../../images/icons/discord.inline.svg'
-import Github from '../../images/icons/github.inline.svg'
-import LinkedIn from '../../images/icons/linkedin.inline.svg'
-import Telegram from '../../images/icons/telegram.inline.svg'
-import Twitter from '../../images/icons/twitter.inline.svg'
-import Youtube from '../../images/icons/youtube.inline.svg'
+import Discord from '@images/icons/discord.inline.svg'
+import Github from '@images/icons/github.inline.svg'
+import LinkedIn from '@images/icons/linkedin.inline.svg'
+import Telegram from '@images/icons/telegram.inline.svg'
+import Twitter from '@images/icons/twitter.inline.svg'
+import Youtube from '@images/icons/youtube.inline.svg'
 
 const SocialLinks = () => {
   return (

+ 2 - 2
governance/xc_admin/packages/xc_admin_frontend/components/layout/Header.tsx

@@ -3,7 +3,7 @@ import Link from 'next/link'
 import { useRouter } from 'next/router'
 import { useContext, useEffect, useState } from 'react'
 import { ClusterContext, DEFAULT_CLUSTER } from '../../contexts/ClusterContext'
-import Pyth from '../../images/logomark.inline.svg'
+import Pyth from '@images/logomark.inline.svg'
 import MobileMenu from './MobileMenu'
 
 const WalletMultiButtonDynamic = dynamic(
@@ -156,7 +156,7 @@ const Header = () => {
           </div>
         </div>
       </header>
-      <MobileMenu headerState={headerState} setHeaderState={setHeaderState} />
+      <MobileMenu headerState={headerState} />
     </>
   )
 }

+ 1 - 1
governance/xc_admin/packages/xc_admin_frontend/components/layout/Layout.tsx

@@ -4,7 +4,7 @@ import Header from './Header'
 
 const Layout = ({ children }: { children: React.ReactNode }) => {
   return (
-    <div className="flex flex-col min-h-screen relative overflow-hidden">
+    <div className="relative flex min-h-screen flex-col overflow-hidden">
       <Header />
       <main className="flex-grow">{children}</main>
       <Footer />

+ 3 - 4
governance/xc_admin/packages/xc_admin_frontend/components/layout/MobileMenu.tsx

@@ -6,15 +6,14 @@ import { useContext, useEffect, useRef } from 'react'
 import { ClusterContext, DEFAULT_CLUSTER } from '../../contexts/ClusterContext'
 import { BurgerState } from './Header'
 
-import orb from '../../images/burger.png'
+import orb from '@images/burger.png'
 
 interface MenuProps {
   headerState: BurgerState
-  setHeaderState: Function
 }
 
-const MobileMenu = ({ headerState, setHeaderState }: MenuProps) => {
-  let burgerMenu = useRef(null)
+const MobileMenu = ({ headerState }: MenuProps) => {
+  const burgerMenu = useRef(null)
   const router = useRouter()
   const { cluster } = useContext(ClusterContext)
 

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

@@ -2,7 +2,7 @@ import { AnchorProvider, Idl, Program } from '@coral-xyz/anchor'
 import { AccountType, getPythProgramKeyForCluster } from '@pythnetwork/client'
 import { PythOracle, pythOracleProgram } from '@pythnetwork/client/lib/anchor'
 import { useWallet } from '@solana/wallet-adapter-react'
-import { Cluster, PublicKey, TransactionInstruction } from '@solana/web3.js'
+import { PublicKey, TransactionInstruction } from '@solana/web3.js'
 import messageBuffer from 'message_buffer/idl/message_buffer.json'
 import { MessageBuffer } from 'message_buffer/idl/message_buffer'
 import axios from 'axios'
@@ -18,7 +18,6 @@ import {
   MESSAGE_BUFFER_PROGRAM_ID,
   MESSAGE_BUFFER_BUFFER_SIZE,
   PRICE_FEED_MULTISIG,
-  WORMHOLE_ADDRESS,
   PRICE_FEED_OPS_KEY,
   getMessageBufferAddressForPrice,
   getMaximumNumberOfPublishers,
@@ -44,8 +43,6 @@ const General = ({ proposerServerUrl }: { proposerServerUrl: string }) => {
     useState(false)
   const { cluster } = useContext(ClusterContext)
   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 { isLoading: isMultisigLoading, squads } = useMultisigContext()
   const { rawConfig, dataIsLoading, connection } = usePythContext()
   const { connected } = useWallet()
@@ -370,7 +367,7 @@ const General = ({ proposerServerUrl }: { proposerServerUrl: string }) => {
           }
 
           // create add publisher instruction if there are any publishers
-          for (let publisherKey of newChanges.priceAccounts[0].publishers) {
+          for (const publisherKey of newChanges.priceAccounts[0].publishers) {
             instructions.push(
               await pythProgramClient.methods
                 .addPublisher(new PublicKey(publisherKey))
@@ -525,7 +522,7 @@ const General = ({ proposerServerUrl }: { proposerServerUrl: string }) => {
 
           // add instructions to remove publishers
 
-          for (let publisherKey of publisherKeysToRemove) {
+          for (const publisherKey of publisherKeysToRemove) {
             instructions.push(
               await pythProgramClient.methods
                 .delPublisher(new PublicKey(publisherKey))
@@ -538,7 +535,7 @@ const General = ({ proposerServerUrl }: { proposerServerUrl: string }) => {
           }
 
           // add instructions to add new publishers
-          for (let publisherKey of publisherKeysToAdd) {
+          for (const publisherKey of publisherKeysToAdd) {
             instructions.push(
               await pythProgramClient.methods
                 .addPublisher(new PublicKey(publisherKey))
@@ -823,7 +820,7 @@ const General = ({ proposerServerUrl }: { proposerServerUrl: string }) => {
 
   // create anchor wallet when connected
   useEffect(() => {
-    if (connected && squads) {
+    if (connected && squads && connection) {
       const provider = new AnchorProvider(
         connection,
         squads.wallet as Wallet,

+ 25 - 0
governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals/InstructionsSummary.tsx

@@ -0,0 +1,25 @@
+import { PythCluster } from '@pythnetwork/client'
+import { MultisigInstruction } from 'xc_admin_common'
+import { getInstructionsSummary } from './utils'
+
+export const InstructionsSummary = ({
+  instructions,
+  cluster,
+}: {
+  instructions: MultisigInstruction[]
+  cluster: PythCluster
+}) => {
+  const instructionsCount = getInstructionsSummary({ instructions, cluster })
+
+  return (
+    <div className="space-y-4">
+      {Object.entries(instructionsCount).map(([name, count]) => {
+        return (
+          <div key={name}>
+            {name}: {count}
+          </div>
+        )
+      })}
+    </div>
+  )
+}

+ 42 - 357
governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals.tsx → governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals/Proposal.tsx

@@ -1,4 +1,4 @@
-import * as Tooltip from '@radix-ui/react-tooltip'
+import NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet'
 import { useWallet } from '@solana/wallet-adapter-react'
 import {
   AccountMeta,
@@ -7,147 +7,47 @@ import {
   SystemProgram,
   TransactionInstruction,
 } from '@solana/web3.js'
+import SquadsMesh from '@sqds/mesh'
 import { MultisigAccount, TransactionAccount } from '@sqds/mesh/lib/types'
-import { useRouter } from 'next/router'
-import { Fragment, useCallback, useContext, useEffect, useState } from 'react'
+import { Fragment, useContext, useEffect, useState } from 'react'
 import toast from 'react-hot-toast'
 import {
+  AnchorMultisigInstruction,
   ExecutePostedVaa,
-  getMultisigCluster,
   MultisigInstruction,
   MultisigParser,
   PythMultisigInstruction,
-  AnchorMultisigInstruction,
   WormholeMultisigInstruction,
   getManyProposalsInstructions,
+  getMultisigCluster,
   getProgramName,
 } from 'xc_admin_common'
-import { ClusterContext } from '../../contexts/ClusterContext'
-import { useMultisigContext } from '../../contexts/MultisigContext'
-import { usePythContext } from '../../contexts/PythContext'
-import { StatusFilterContext } from '../../contexts/StatusFilterContext'
-import VerifiedIcon from '../../images/icons/verified.inline.svg'
-import WarningIcon from '../../images/icons/warning.inline.svg'
-import VotedIcon from '../../images/icons/voted.inline.svg'
-import { capitalizeFirstLetter } from '../../utils/capitalizeFirstLetter'
-import ClusterSwitch from '../ClusterSwitch'
-import CopyText from '../common/CopyText'
-import Spinner from '../common/Spinner'
-import Loadbar from '../loaders/Loadbar'
-import ProposalStatusFilter from '../ProposalStatusFilter'
-import SquadsMesh from '@sqds/mesh'
-import NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet'
-import { WormholeInstructionView } from '../InstructionViews/WormholeInstructionView'
+import { ClusterContext } from '../../../contexts/ClusterContext'
+import { useMultisigContext } from '../../../contexts/MultisigContext'
+import { usePythContext } from '../../../contexts/PythContext'
+import { capitalizeFirstLetter } from '../../../utils/capitalizeFirstLetter'
 import {
   ParsedAccountPubkeyRow,
   SignerTag,
   WritableTag,
-} from '../InstructionViews/AccountUtils'
+} from '../../InstructionViews/AccountUtils'
+import { WormholeInstructionView } from '../../InstructionViews/WormholeInstructionView'
+import CopyText from '../../common/CopyText'
+import Spinner from '../../common/Spinner'
+import Loadbar from '../../loaders/Loadbar'
 
-import { getMappingCluster, isPubkey } from '../InstructionViews/utils'
-import { getPythProgramKeyForCluster, PythCluster } from '@pythnetwork/client'
-import {
-  DEFAULT_PRIORITY_FEE_CONFIG,
-  TransactionBuilder,
-  sendTransactions,
-} from '@pythnetwork/solana-utils'
 import { Wallet } from '@coral-xyz/anchor'
-const ProposalRow = ({
-  proposal,
-  multisig,
-}: {
-  proposal: TransactionAccount
-  multisig: MultisigAccount | undefined
-}) => {
-  const status = getProposalStatus(proposal, multisig)
-
-  const router = useRouter()
-
-  const handleClickIndividualProposal = useCallback(
-    (proposalPubkey: string) => {
-      router.query.proposal = proposalPubkey
-      router.push(
-        {
-          pathname: router.pathname,
-          query: router.query,
-        },
-        undefined,
-        { scroll: true }
-      )
-    },
-    [router]
-  )
-  return (
-    <div
-      className="my-2 max-h-[58px] cursor-pointer bg-[#1E1B2F] hover:bg-darkGray2"
-      onClick={() =>
-        handleClickIndividualProposal(proposal.publicKey.toBase58())
-      }
-    >
-      <div className="flex justify-between p-4">
-        <div className="flex">
-          <span className="mr-2 hidden sm:block">
-            {proposal.publicKey.toBase58()}
-          </span>
-          <span className="mr-2 sm:hidden">
-            {proposal.publicKey.toBase58().slice(0, 6) +
-              '...' +
-              proposal.publicKey.toBase58().slice(-6)}
-          </span>{' '}
-        </div>
-        <div className="flex space-x-2">
-          {proposal.approved.length > 0 && status === 'active' && (
-            <div>
-              <StatusTag
-                proposalStatus="executed"
-                text={`Approved: ${proposal.approved.length}`}
-              />
-            </div>
-          )}
-          {proposal.rejected.length > 0 && status === 'active' && (
-            <div>
-              <StatusTag
-                proposalStatus="rejected"
-                text={`Rejected: ${proposal.rejected.length}`}
-              />
-            </div>
-          )}
-          <div>
-            <StatusTag proposalStatus={status} />
-          </div>
-        </div>
-      </div>
-    </div>
-  )
-}
-
-const StatusTag = ({
-  proposalStatus,
-  text,
-}: {
-  proposalStatus: string
-  text?: string
-}) => {
-  return (
-    <div
-      className={`flex items-center justify-center rounded-full ${
-        proposalStatus === 'active'
-          ? 'bg-[#3C3299]'
-          : proposalStatus === 'executed'
-          ? 'bg-[#1B730E]'
-          : proposalStatus === 'cancelled'
-          ? 'bg-[#C4428F]'
-          : proposalStatus === 'rejected'
-          ? 'bg-[#CF6E42]'
-          : proposalStatus === 'expired'
-          ? 'bg-[#A52A2A]'
-          : 'bg-pythPurple'
-      } py-1 px-2 text-xs`}
-    >
-      {text || proposalStatus}
-    </div>
-  )
-}
+import { PythCluster, getPythProgramKeyForCluster } from '@pythnetwork/client'
+import { TransactionBuilder, sendTransactions } from '@pythnetwork/solana-utils'
+import { getMappingCluster, isPubkey } from '../../InstructionViews/utils'
+import { StatusTag } from './StatusTag'
+import { getProposalStatus } from './utils'
+
+import VerifiedIcon from '@images/icons/verified.inline.svg'
+import VotedIcon from '@images/icons/voted.inline.svg'
+import WarningIcon from '@images/icons/warning.inline.svg'
+import * as Tooltip from '@radix-ui/react-tooltip'
+import { InstructionsSummary } from './InstructionsSummary'
 
 const IconWithTooltip = ({
   icon,
@@ -194,26 +94,11 @@ const VotedIconWithTooltip = () => {
   return (
     <IconWithTooltip
       icon={<VotedIcon />}
-      tooltipText=" You have voted on this proposal."
+      tooltipText="You have voted on this proposal."
     />
   )
 }
 
-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 AccountList = ({
   listName,
   accounts,
@@ -244,14 +129,12 @@ const AccountList = ({
   )
 }
 
-type ProposalType = 'priceFeed' | 'governance'
-
-const Proposal = ({
+export const Proposal = ({
   proposal,
   multisig,
 }: {
-  proposal: TransactionAccount | undefined
-  multisig: MultisigAccount | undefined
+  proposal?: TransactionAccount
+  multisig?: MultisigAccount
 }) => {
   const [instructions, setInstructions] = useState<MultisigInstruction[]>([])
   const [isTransactionLoading, setIsTransactionLoading] = useState(false)
@@ -477,9 +360,14 @@ const Proposal = ({
     )
   }
 
-  return proposal !== undefined &&
-    multisig !== undefined &&
-    !isMultisigLoading ? (
+  if (!proposal || !multisig || isMultisigLoading)
+    return (
+      <div className="mt-6">
+        <Loadbar theme="light" />
+      </div>
+    )
+
+  return (
     <div className="grid grid-cols-3 gap-4">
       <div className="col-span-3 my-2 space-y-4 bg-[#1E1B2F] p-4">
         <h4 className="h4 font-semibold">
@@ -599,6 +487,9 @@ const Proposal = ({
           Total Instructions: {instructions.length}
         </h4>
         <hr className="border-gray-700" />
+        <h4 className="h4 text-[20px] font-semibold">Summary</h4>
+        <InstructionsSummary instructions={instructions} cluster={cluster} />
+        <hr className="border-gray-700" />
         {instructions?.map((instruction, index) => (
           <Fragment key={index}>
             <h4 className="h4 text-[20px] font-semibold">
@@ -659,6 +550,8 @@ const Proposal = ({
                                 ? instruction.args[key]
                                 : instruction.args[key] instanceof Uint8Array
                                 ? instruction.args[key].toString('hex')
+                                : typeof instruction.args[key] === 'bigint'
+                                ? instruction.args[key].toString()
                                 : JSON.stringify(instruction.args[key])}
                             </div>
                           )}
@@ -764,213 +657,5 @@ const Proposal = ({
         ))}
       </div>
     </div>
-  ) : (
-    <div className="mt-6">
-      <Loadbar theme="light" />
-    </div>
-  )
-}
-
-const Proposals = () => {
-  const router = useRouter()
-  const { connected, publicKey: signerPublicKey } = useWallet()
-  const [currentProposal, setCurrentProposal] = useState<TransactionAccount>()
-  const [currentProposalPubkey, setCurrentProposalPubkey] = useState<string>()
-  const { cluster } = useContext(ClusterContext)
-  const { statusFilter } = useContext(StatusFilterContext)
-
-  const {
-    upgradeMultisigAccount,
-    priceFeedMultisigAccount,
-    priceFeedMultisigProposals,
-    upgradeMultisigProposals,
-    isLoading: isMultisigLoading,
-    refreshData,
-  } = useMultisigContext()
-
-  const [proposalType, setProposalType] = useState<ProposalType>('priceFeed')
-
-  const multisigAccount =
-    proposalType === 'priceFeed'
-      ? priceFeedMultisigAccount
-      : upgradeMultisigAccount
-  const multisigProposals =
-    proposalType === 'priceFeed'
-      ? priceFeedMultisigProposals
-      : upgradeMultisigProposals
-  const [filteredProposals, setFilteredProposals] = useState<
-    TransactionAccount[]
-  >([])
-
-  const handleClickBackToProposals = () => {
-    delete router.query.proposal
-    router.push(
-      {
-        pathname: router.pathname,
-        query: router.query,
-      },
-      undefined,
-      { scroll: false }
-    )
-  }
-
-  useEffect(() => {
-    if (router.query.proposal) {
-      setCurrentProposalPubkey(router.query.proposal as string)
-    } else {
-      setCurrentProposalPubkey(undefined)
-    }
-  }, [router.query.proposal])
-
-  const switchProposalType = useCallback(() => {
-    if (proposalType === 'priceFeed') {
-      setProposalType('governance')
-    } else {
-      setProposalType('priceFeed')
-    }
-  }, [proposalType])
-
-  useEffect(() => {
-    if (currentProposalPubkey) {
-      const currProposal = multisigProposals.find(
-        (proposal) => proposal.publicKey.toBase58() === currentProposalPubkey
-      )
-      setCurrentProposal(currProposal)
-      if (currProposal === undefined) {
-        const otherProposals =
-          proposalType !== 'priceFeed'
-            ? priceFeedMultisigProposals
-            : upgradeMultisigProposals
-        if (
-          otherProposals.findIndex(
-            (proposal) =>
-              proposal.publicKey.toBase58() === currentProposalPubkey
-          ) !== -1
-        ) {
-          switchProposalType()
-        }
-      }
-    }
-  }, [
-    switchProposalType,
-    priceFeedMultisigProposals,
-    proposalType,
-    upgradeMultisigProposals,
-    currentProposalPubkey,
-    multisigProposals,
-    cluster,
-  ])
-
-  useEffect(() => {
-    // filter price feed multisig proposals by status
-    if (statusFilter === 'all') {
-      setFilteredProposals(multisigProposals)
-    } else {
-      setFilteredProposals(
-        multisigProposals.filter(
-          (proposal) =>
-            getProposalStatus(proposal, multisigAccount) === statusFilter
-        )
-      )
-    }
-  }, [statusFilter, multisigAccount, multisigProposals])
-
-  return (
-    <div className="relative">
-      <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">
-            {proposalType === 'priceFeed' ? 'Price Feed ' : 'Governance '}{' '}
-            {router.query.proposal === undefined ? 'Proposals' : 'Proposal'}
-          </h1>
-        </div>
-      </div>
-      <div className="container min-h-[50vh]">
-        {router.query.proposal === undefined ? (
-          <>
-            <div className="flex flex-col justify-between md:flex-row">
-              <div className="mb-4 flex items-center md:mb-0">
-                <ClusterSwitch />
-              </div>
-              <div className="flex space-x-2">
-                {refreshData && (
-                  <button
-                    disabled={isMultisigLoading}
-                    className="sub-action-btn text-base"
-                    onClick={() => {
-                      const { fetchData } = refreshData()
-                      fetchData()
-                    }}
-                  >
-                    Refresh
-                  </button>
-                )}
-                <button
-                  disabled={isMultisigLoading}
-                  className="action-btn text-base"
-                  onClick={switchProposalType}
-                >
-                  Show
-                  {proposalType !== 'priceFeed'
-                    ? ' Price Feed '
-                    : ' Governance '}
-                  Proposals
-                </button>
-              </div>
-            </div>
-            <div className="relative mt-6">
-              {isMultisigLoading ? (
-                <div className="mt-3">
-                  <Loadbar theme="light" />
-                </div>
-              ) : (
-                <>
-                  <div className="flex items-center justify-between pb-4">
-                    <h4 className="h4">
-                      Total Proposals: {filteredProposals.length}
-                    </h4>
-                    <ProposalStatusFilter />
-                  </div>
-                  {filteredProposals.length > 0 ? (
-                    <div className="flex flex-col">
-                      {filteredProposals.map((proposal, idx) => (
-                        <ProposalRow
-                          key={proposal.publicKey.toBase58()}
-                          proposal={proposal}
-                          multisig={multisigAccount}
-                        />
-                      ))}
-                    </div>
-                  ) : (
-                    <div className="mt-4">
-                      No proposals found. If you&apos;re a member of the
-                      multisig, you can create a proposal.
-                    </div>
-                  )}
-                </>
-              )}
-            </div>
-          </>
-        ) : !isMultisigLoading && currentProposal !== undefined ? (
-          <>
-            <div
-              className="max-w-fit cursor-pointer bg-darkGray2 p-3 text-xs font-semibold outline-none transition-colors hover:bg-darkGray3 md:text-base"
-              onClick={handleClickBackToProposals}
-            >
-              &#8592; back to proposals
-            </div>
-            <div className="relative mt-6">
-              <Proposal proposal={currentProposal} multisig={multisigAccount} />
-            </div>
-          </>
-        ) : (
-          <div className="mt-3">
-            <Loadbar theme="light" />
-          </div>
-        )}
-      </div>
-    </div>
   )
 }
-
-export default Proposals

+ 191 - 0
governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals/ProposalRow.tsx

@@ -0,0 +1,191 @@
+import SquadsMesh from '@sqds/mesh'
+import { MultisigAccount, TransactionAccount } from '@sqds/mesh/lib/types'
+import { useRouter } from 'next/router'
+import { useCallback, useContext, useEffect, useRef, useState } from 'react'
+import { getMultisigCluster } from 'xc_admin_common'
+import { ClusterContext } from '../../../contexts/ClusterContext'
+import { useMultisigContext } from '../../../contexts/MultisigContext'
+import { StatusTag } from './StatusTag'
+import { getInstructionsSummary, getProposalStatus } from './utils'
+
+import NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet'
+import { AccountMeta, Keypair } from '@solana/web3.js'
+import { MultisigParser, getManyProposalsInstructions } from 'xc_admin_common'
+
+export const ProposalRow = ({
+  proposal,
+  multisig,
+}: {
+  proposal: TransactionAccount
+  multisig: MultisigAccount | undefined
+}) => {
+  const [time, setTime] = useState<Date>()
+  const [instructions, setInstructions] = useState<[string, number][]>()
+  const status = getProposalStatus(proposal, multisig)
+  const { cluster } = useContext(ClusterContext)
+  const { isLoading: isMultisigLoading, connection } = useMultisigContext()
+  const router = useRouter()
+  const elementRef = useRef(null)
+  const formattedTime = time?.toLocaleString(undefined, {
+    year: 'numeric',
+    month: 'short',
+    day: 'numeric',
+    hour: 'numeric',
+    minute: 'numeric',
+    hour12: false,
+  })
+
+  /**
+   * Fetch the block time of the first transaction of the proposal
+   * and calculates the instructions summary of the proposal
+   * when the proposal is in view
+   */
+  useEffect(() => {
+    let isCancelled = false
+    const element = elementRef.current
+    const observer = new IntersectionObserver(async (entries) => {
+      if (entries[0].isIntersecting) {
+        if (isMultisigLoading || !connection) {
+          return
+        }
+
+        // set proposal time
+        if (!time) {
+          connection
+            .getConfirmedSignaturesForAddress2(proposal.publicKey)
+            .then((txs) => {
+              if (isCancelled) return
+              const firstBlockTime = txs?.[txs.length - 1]?.blockTime
+              if (firstBlockTime) {
+                setTime(new Date(firstBlockTime * 1000))
+              }
+            })
+        }
+
+        // calculate instructions summary
+        if (!instructions) {
+          const readOnlySquads = new SquadsMesh({
+            connection,
+            wallet: new NodeWallet(new Keypair()),
+          })
+          const proposalInstructions = (
+            await getManyProposalsInstructions(readOnlySquads, [proposal])
+          )[0]
+          const multisigParser = MultisigParser.fromCluster(
+            getMultisigCluster(cluster)
+          )
+          const parsedInstructions = proposalInstructions.map((ix) =>
+            multisigParser.parseInstruction({
+              programId: ix.programId,
+              data: ix.data as Buffer,
+              keys: ix.keys as AccountMeta[],
+            })
+          )
+
+          const summary = getInstructionsSummary({
+            instructions: parsedInstructions,
+            cluster,
+          })
+
+          // show only the first two instructions
+          // and group the rest under 'other'
+          const shortSummary = Object.entries(summary).slice(0, 2)
+          const otherValue = Object.values(summary)
+            .slice(2)
+            .reduce((acc, curr) => acc + curr, 0)
+          const updatedSummary = [
+            ...shortSummary,
+            ...(otherValue > 0
+              ? ([['other', otherValue]] as [string, number][])
+              : []),
+          ]
+
+          if (!isCancelled) {
+            setInstructions(updatedSummary)
+          }
+        }
+      }
+    })
+
+    if (element) {
+      observer.observe(element)
+    }
+
+    // Clean up function
+    return () => {
+      isCancelled = true
+      if (element) {
+        observer.unobserve(element)
+      }
+    }
+  }, [time, cluster, proposal, connection, isMultisigLoading, instructions])
+
+  const handleClickIndividualProposal = useCallback(
+    (proposalPubkey: string) => {
+      router.query.proposal = proposalPubkey
+      router.push(
+        {
+          pathname: router.pathname,
+          query: router.query,
+        },
+        undefined,
+        { scroll: true }
+      )
+    },
+    [router]
+  )
+
+  return (
+    <div
+      ref={elementRef}
+      className="my-2 cursor-pointer bg-[#1E1B2F] hover:bg-darkGray2"
+      onClick={() =>
+        handleClickIndividualProposal(proposal.publicKey.toBase58())
+      }
+    >
+      <div className="flex flex-wrap gap-4 p-4">
+        <div className="font-bold">{proposal.transactionIndex}</div>
+        <div className="flex items-center">
+          {formattedTime ?? (
+            <div className="h-5 w-48 animate-pulse rounded bg-beige-300" />
+          )}
+        </div>
+        <div className="flex">
+          <span className="mr-2">
+            {proposal.publicKey.toBase58().slice(0, 6) +
+              '...' +
+              proposal.publicKey.toBase58().slice(-6)}
+          </span>{' '}
+        </div>
+        <div className="flex flex-grow gap-4">
+          {instructions?.map(([name, count]) => (
+            <div key={name}>
+              {name}: {count}
+            </div>
+          ))}
+        </div>
+        <div className="flex space-x-2">
+          {proposal.approved.length > 0 && status === 'active' && (
+            <div>
+              <StatusTag
+                proposalStatus="executed"
+                text={`Approved: ${proposal.approved.length}`}
+              />
+            </div>
+          )}
+          {proposal.rejected.length > 0 && status === 'active' && (
+            <div>
+              <StatusTag
+                proposalStatus="rejected"
+                text={`Rejected: ${proposal.rejected.length}`}
+              />
+            </div>
+          )}
+          <div>
+            <StatusTag proposalStatus={status} />
+          </div>
+        </div>
+      </div>
+    </div>
+  )
+}

+ 218 - 0
governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals/Proposals.tsx

@@ -0,0 +1,218 @@
+import { TransactionAccount } from '@sqds/mesh/lib/types'
+import { useRouter } from 'next/router'
+import { useCallback, useContext, useEffect, useState } from 'react'
+import { ClusterContext } from '../../../contexts/ClusterContext'
+import { useMultisigContext } from '../../../contexts/MultisigContext'
+import { StatusFilterContext } from '../../../contexts/StatusFilterContext'
+import ClusterSwitch from '../../ClusterSwitch'
+import ProposalStatusFilter from '../../ProposalStatusFilter'
+import Loadbar from '../../loaders/Loadbar'
+
+import { ProposalRow } from './ProposalRow'
+import { getProposalStatus } from './utils'
+import { Proposal } from './Proposal'
+
+type ProposalType = 'priceFeed' | 'governance'
+
+const Proposals = () => {
+  const router = useRouter()
+  const [currentProposal, setCurrentProposal] = useState<TransactionAccount>()
+  const [currentProposalPubkey, setCurrentProposalPubkey] = useState<string>()
+  const { cluster } = useContext(ClusterContext)
+  const { statusFilter } = useContext(StatusFilterContext)
+
+  const {
+    upgradeMultisigAccount,
+    priceFeedMultisigAccount,
+    priceFeedMultisigProposals,
+    upgradeMultisigProposals,
+    isLoading: isMultisigLoading,
+    refreshData,
+  } = useMultisigContext()
+
+  const [proposalType, setProposalType] = useState<ProposalType>('priceFeed')
+
+  const multisigAccount =
+    proposalType === 'priceFeed'
+      ? priceFeedMultisigAccount
+      : upgradeMultisigAccount
+  const multisigProposals =
+    proposalType === 'priceFeed'
+      ? priceFeedMultisigProposals
+      : upgradeMultisigProposals
+  const [filteredProposals, setFilteredProposals] = useState<
+    TransactionAccount[]
+  >([])
+
+  const handleClickBackToProposals = () => {
+    delete router.query.proposal
+    router.push(
+      {
+        pathname: router.pathname,
+        query: router.query,
+      },
+      undefined,
+      { scroll: false }
+    )
+  }
+
+  useEffect(() => {
+    if (router.query.proposal) {
+      setCurrentProposalPubkey(router.query.proposal as string)
+    } else {
+      setCurrentProposalPubkey(undefined)
+    }
+  }, [router.query.proposal])
+
+  const switchProposalType = useCallback(() => {
+    if (proposalType === 'priceFeed') {
+      setProposalType('governance')
+    } else {
+      setProposalType('priceFeed')
+    }
+  }, [proposalType])
+
+  useEffect(() => {
+    if (currentProposalPubkey) {
+      const currProposal = multisigProposals.find(
+        (proposal) => proposal.publicKey.toBase58() === currentProposalPubkey
+      )
+      setCurrentProposal(currProposal)
+      if (currProposal === undefined) {
+        const otherProposals =
+          proposalType !== 'priceFeed'
+            ? priceFeedMultisigProposals
+            : upgradeMultisigProposals
+        if (
+          otherProposals.findIndex(
+            (proposal) =>
+              proposal.publicKey.toBase58() === currentProposalPubkey
+          ) !== -1
+        ) {
+          switchProposalType()
+        }
+      }
+    }
+  }, [
+    switchProposalType,
+    priceFeedMultisigProposals,
+    proposalType,
+    upgradeMultisigProposals,
+    currentProposalPubkey,
+    multisigProposals,
+    cluster,
+  ])
+
+  useEffect(() => {
+    // filter price feed multisig proposals by status
+    if (statusFilter === 'all') {
+      setFilteredProposals(multisigProposals)
+    } else {
+      setFilteredProposals(
+        multisigProposals.filter(
+          (proposal) =>
+            getProposalStatus(proposal, multisigAccount) === statusFilter
+        )
+      )
+    }
+  }, [statusFilter, multisigAccount, multisigProposals])
+
+  return (
+    <div className="relative">
+      <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">
+            {proposalType === 'priceFeed' ? 'Price Feed ' : 'Governance '}{' '}
+            {router.query.proposal === undefined ? 'Proposals' : 'Proposal'}
+          </h1>
+        </div>
+      </div>
+      <div className="container min-h-[50vh]">
+        {router.query.proposal === undefined ? (
+          <>
+            <div className="flex flex-col justify-between md:flex-row">
+              <div className="mb-4 flex items-center md:mb-0">
+                <ClusterSwitch />
+              </div>
+              <div className="flex space-x-2">
+                {refreshData && (
+                  <button
+                    disabled={isMultisigLoading}
+                    className="sub-action-btn text-base"
+                    onClick={() => {
+                      const { fetchData } = refreshData()
+                      fetchData()
+                    }}
+                  >
+                    Refresh
+                  </button>
+                )}
+                <button
+                  disabled={isMultisigLoading}
+                  className="action-btn text-base"
+                  onClick={switchProposalType}
+                >
+                  Show
+                  {proposalType !== 'priceFeed'
+                    ? ' Price Feed '
+                    : ' Governance '}
+                  Proposals
+                </button>
+              </div>
+            </div>
+            <div className="relative mt-6">
+              {isMultisigLoading ? (
+                <div className="mt-3">
+                  <Loadbar theme="light" />
+                </div>
+              ) : (
+                <>
+                  <div className="flex items-center justify-between pb-4">
+                    <h4 className="h4">
+                      Total Proposals: {filteredProposals.length}
+                    </h4>
+                    <ProposalStatusFilter />
+                  </div>
+                  {filteredProposals.length > 0 ? (
+                    <div className="flex flex-col">
+                      {filteredProposals.map((proposal, _idx) => (
+                        <ProposalRow
+                          key={proposal.publicKey.toBase58()}
+                          proposal={proposal}
+                          multisig={multisigAccount}
+                        />
+                      ))}
+                    </div>
+                  ) : (
+                    <div className="mt-4">
+                      No proposals found. If you&apos;re a member of the
+                      multisig, you can create a proposal.
+                    </div>
+                  )}
+                </>
+              )}
+            </div>
+          </>
+        ) : !isMultisigLoading && currentProposal !== undefined ? (
+          <>
+            <div
+              className="max-w-fit cursor-pointer bg-darkGray2 p-3 text-xs font-semibold outline-none transition-colors hover:bg-darkGray3 md:text-base"
+              onClick={handleClickBackToProposals}
+            >
+              &#8592; back to proposals
+            </div>
+            <div className="relative mt-6">
+              <Proposal proposal={currentProposal} multisig={multisigAccount} />
+            </div>
+          </>
+        ) : (
+          <div className="mt-3">
+            <Loadbar theme="light" />
+          </div>
+        )}
+      </div>
+    </div>
+  )
+}
+
+export default Proposals

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

@@ -0,0 +1,37 @@
+import { ProposalStatus } from './utils'
+
+const getProposalBackgroundColorClassName = (
+  proposalStatus: ProposalStatus
+) => {
+  if (proposalStatus === 'active') {
+    return 'bg-[#3C3299]'
+  } else if (proposalStatus === 'executed') {
+    return 'bg-[#1B730E]'
+  } else if (proposalStatus === 'cancelled') {
+    return 'bg-[#C4428F]'
+  } else if (proposalStatus === 'rejected') {
+    return 'bg-[#CF6E42]'
+  } else if (proposalStatus === 'expired') {
+    return 'bg-[#A52A2A]'
+  } else {
+    return 'bg-pythPurple'
+  }
+}
+
+export const StatusTag = ({
+  proposalStatus,
+  text,
+}: {
+  proposalStatus: ProposalStatus
+  text?: string
+}) => {
+  return (
+    <div
+      className={`flex items-center justify-center rounded-full ${getProposalBackgroundColorClassName(
+        proposalStatus
+      )} py-1 px-2 text-xs`}
+    >
+      {text || proposalStatus}
+    </div>
+  )
+}

+ 100 - 0
governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals/utils.ts

@@ -0,0 +1,100 @@
+import { PythCluster } from '@pythnetwork/client'
+import { AccountMeta } from '@solana/web3.js'
+import { MultisigAccount, TransactionAccount } from '@sqds/mesh/lib/types'
+import {
+  ExecutePostedVaa,
+  MultisigInstruction,
+  MultisigParser,
+  PythGovernanceActionImpl,
+  SetDataSources,
+  WormholeMultisigInstruction,
+} from 'xc_admin_common'
+
+export type ProposalStatus =
+  | 'active'
+  | 'executed'
+  | 'cancelled'
+  | 'rejected'
+  | 'expired'
+  | 'executeReady'
+  | 'draft'
+  | 'unkwown'
+
+export const getProposalStatus = (
+  proposal: TransactionAccount | undefined,
+  multisig: MultisigAccount | undefined
+): ProposalStatus => {
+  if (multisig && proposal) {
+    const onChainStatus = Object.keys(proposal.status)[0]
+    return proposal.transactionIndex <= multisig.msChangeIndex &&
+      (onChainStatus == 'active' || onChainStatus == 'draft')
+      ? 'expired'
+      : (onChainStatus as ProposalStatus)
+  } else {
+    return 'unkwown'
+  }
+}
+
+/**
+ * Sorts the properties of an object by their values in ascending order.
+ *
+ * @param {Record<string, number>} obj - The object to sort. All property values should be numbers.
+ * @returns {Record<string, number>} A new object with the same properties as the input, but ordered such that the property with the largest numerical value comes first.
+ *
+ * @example
+ * const obj = { a: 2, b: 3, c: 1 };
+ * const sortedObj = sortObjectByValues(obj);
+ * console.log(sortedObj); // Outputs: { b: 3, a: 2, c: 1 }
+ */
+const sortObjectByValues = (obj: Record<string, number>) => {
+  const sortedEntries = Object.entries(obj).sort(([, a], [, b]) => b - a)
+  const sortedObj: Record<string, number> = {}
+  for (const [key, value] of sortedEntries) {
+    sortedObj[key] = value
+  }
+  return sortedObj
+}
+
+/**
+ * Returns a summary of the instructions in a list of multisig instructions.
+ *
+ * @param {MultisigInstruction[]} options.instructions - The list of multisig instructions to summarize.
+ * @param {PythCluster} options.cluster - The Pyth cluster to use for parsing instructions.
+ * @returns {Record<string, number>} A summary of the instructions, where the keys are the names of the instructions and the values are the number of times each instruction appears in the list.
+ */
+export const getInstructionsSummary = (options: {
+  instructions: MultisigInstruction[]
+  cluster: PythCluster
+}) => {
+  const { instructions, cluster } = options
+
+  return sortObjectByValues(
+    instructions.reduce((acc, instruction) => {
+      if (instruction instanceof WormholeMultisigInstruction) {
+        const governanceAction = instruction.governanceAction
+        if (governanceAction instanceof ExecutePostedVaa) {
+          const innerInstructions = governanceAction.instructions
+          innerInstructions.forEach((innerInstruction) => {
+            const multisigParser = MultisigParser.fromCluster(cluster)
+            const parsedInstruction = multisigParser.parseInstruction({
+              programId: innerInstruction.programId,
+              data: innerInstruction.data as Buffer,
+              keys: innerInstruction.keys as AccountMeta[],
+            })
+            acc[parsedInstruction.name] = (acc[parsedInstruction.name] ?? 0) + 1
+          })
+        } else if (governanceAction instanceof PythGovernanceActionImpl) {
+          acc[governanceAction.action] = (acc[governanceAction.action] ?? 0) + 1
+        } else if (governanceAction instanceof SetDataSources) {
+          acc[governanceAction.actionName] =
+            (acc[governanceAction.actionName] ?? 0) + 1
+        } else {
+          acc['unknown'] = (acc['unknown'] ?? 0) + 1
+        }
+      } else {
+        acc[instruction.name] = (acc[instruction.name] ?? 0) + 1
+      }
+      return acc
+    }, {} as Record<string, number>)
+  )
+}

+ 2 - 3
governance/xc_admin/packages/xc_admin_frontend/components/tabs/UpdatePermissions.tsx

@@ -21,14 +21,13 @@ import {
   getMultisigCluster,
   isRemoteCluster,
   mapKey,
-  WORMHOLE_ADDRESS,
   UPGRADE_MULTISIG,
   MultisigVault,
 } from 'xc_admin_common'
 import { ClusterContext } from '../../contexts/ClusterContext'
 import { useMultisigContext } from '../../contexts/MultisigContext'
 import { usePythContext } from '../../contexts/PythContext'
-import CopyIcon from '../../images/icons/copy.inline.svg'
+import CopyIcon from '@images/icons/copy.inline.svg'
 import { capitalizeFirstLetter } from '../../utils/capitalizeFirstLetter'
 import ClusterSwitch from '../ClusterSwitch'
 import Modal from '../common/Modal'
@@ -336,7 +335,7 @@ const UpdatePermissions = () => {
 
   // create anchor wallet when connected
   useEffect(() => {
-    if (connected && squads) {
+    if (connected && squads && connection) {
       const provider = new AnchorProvider(
         connection,
         squads.wallet as Wallet,

+ 9 - 5
governance/xc_admin/packages/xc_admin_frontend/contexts/ClusterContext.tsx

@@ -1,17 +1,17 @@
 import { PythCluster } from '@pythnetwork/client/lib/cluster'
-import { createContext, useMemo, useState } from 'react'
+import { ReactNode, createContext, useMemo, useState } from 'react'
 
 export const DEFAULT_CLUSTER: PythCluster = 'mainnet-beta'
 
 export const ClusterContext = createContext<{
   cluster: PythCluster
-  setCluster: any
+  setCluster: (_cluster: PythCluster) => void
 }>({
   cluster: DEFAULT_CLUSTER,
-  setCluster: {},
+  setCluster: () => {},
 })
 
-export const ClusterProvider = (props: any) => {
+export const ClusterProvider = ({ children }: { children: ReactNode }) => {
   const [cluster, setCluster] = useState<PythCluster>(DEFAULT_CLUSTER)
   const contextValue = useMemo(
     () => ({
@@ -22,5 +22,9 @@ export const ClusterProvider = (props: any) => {
     }),
     [cluster]
   )
-  return <ClusterContext.Provider {...props} value={contextValue} />
+  return (
+    <ClusterContext.Provider value={contextValue}>
+      {children}
+    </ClusterContext.Provider>
+  )
 }

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

@@ -1,17 +1,12 @@
-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, MultisigHookData } from '../hooks/useMultisig'
+import { MultisigHookData, useMultisig } from '../hooks/useMultisig'
 
 const MultisigContext = createContext<MultisigHookData>({
   upgradeMultisigAccount: undefined,
   priceFeedMultisigAccount: undefined,
   upgradeMultisigProposals: [],
   priceFeedMultisigProposals: [],
-  allProposalsIxsParsed: [],
   isLoading: true,
-  error: null,
   squads: undefined,
   refreshData: undefined,
   connection: undefined,
@@ -28,13 +23,11 @@ export const MultisigContextProvider: React.FC<
 > = ({ children }) => {
   const {
     isLoading,
-    error,
     squads,
     upgradeMultisigAccount,
     priceFeedMultisigAccount,
     upgradeMultisigProposals,
     priceFeedMultisigProposals,
-    allProposalsIxsParsed,
     refreshData,
     connection,
   } = useMultisig()
@@ -45,9 +38,7 @@ export const MultisigContextProvider: React.FC<
       priceFeedMultisigAccount,
       upgradeMultisigProposals,
       priceFeedMultisigProposals,
-      allProposalsIxsParsed,
       isLoading,
-      error,
       squads,
       refreshData,
       connection,
@@ -55,12 +46,10 @@ export const MultisigContextProvider: React.FC<
     [
       squads,
       isLoading,
-      error,
       upgradeMultisigAccount,
       priceFeedMultisigAccount,
       upgradeMultisigProposals,
       priceFeedMultisigProposals,
-      allProposalsIxsParsed,
       refreshData,
       connection,
     ]

+ 4 - 9
governance/xc_admin/packages/xc_admin_frontend/contexts/PythContext.tsx

@@ -5,16 +5,15 @@ import React, {
   useMemo,
   useState,
 } from 'react'
-import usePyth from '../hooks/usePyth'
+import { usePyth } from '../hooks/usePyth'
 import { RawConfig } from '../hooks/usePyth'
+import { Connection } from '@solana/web3.js'
 
-// TODO: fix any
 type AccountKeyToSymbol = { [key: string]: string }
 interface PythContextProps {
   rawConfig: RawConfig
   dataIsLoading: boolean
-  error: any
-  connection: any
+  connection?: Connection
   priceAccountKeyToSymbolMapping: AccountKeyToSymbol
   productAccountKeyToSymbolMapping: AccountKeyToSymbol
   publisherKeyToNameMapping: Record<string, Record<string, string>>
@@ -24,8 +23,6 @@ interface PythContextProps {
 const PythContext = createContext<PythContextProps>({
   rawConfig: { mappingAccounts: [] },
   dataIsLoading: true,
-  error: null,
-  connection: null,
   priceAccountKeyToSymbolMapping: {},
   productAccountKeyToSymbolMapping: {},
   publisherKeyToNameMapping: {},
@@ -44,7 +41,7 @@ export const PythContextProvider: React.FC<PythContextProviderProps> = ({
   publisherKeyToNameMapping,
   multisigSignerKeyToNameMapping,
 }) => {
-  const { isLoading, error, connection, rawConfig } = usePyth()
+  const { isLoading, connection, rawConfig } = usePyth()
   const [
     productAccountKeyToSymbolMapping,
     setProductAccountKeyToSymbolMapping,
@@ -72,7 +69,6 @@ export const PythContextProvider: React.FC<PythContextProviderProps> = ({
     () => ({
       rawConfig,
       dataIsLoading: isLoading,
-      error,
       connection,
       priceAccountKeyToSymbolMapping,
       productAccountKeyToSymbolMapping,
@@ -82,7 +78,6 @@ export const PythContextProvider: React.FC<PythContextProviderProps> = ({
     [
       rawConfig,
       isLoading,
-      error,
       connection,
       publisherKeyToNameMapping,
       multisigSignerKeyToNameMapping,

+ 15 - 8
governance/xc_admin/packages/xc_admin_frontend/contexts/StatusFilterContext.tsx

@@ -1,27 +1,34 @@
-import { createContext, useMemo, useState } from 'react'
+import { ReactNode, createContext, useMemo, useState } from 'react'
+import { ProposalStatus } from '../components/tabs/Proposals/utils'
 
 export const DEFAULT_STATUS_FILTER = 'all'
 
+export type ProposalStatusFilter = 'all' | ProposalStatus
+
 export const StatusFilterContext = createContext<{
-  statusFilter: string
-  setStatusFilter: any
+  statusFilter: ProposalStatusFilter
+  setStatusFilter: (_statusFilter: ProposalStatusFilter) => void
 }>({
   statusFilter: DEFAULT_STATUS_FILTER,
-  setStatusFilter: {},
+  setStatusFilter: () => {},
 })
 
-export const StatusFilterProvider = (props: any) => {
-  const [statusFilter, setStatusFilter] = useState<string>(
+export const StatusFilterProvider = ({ children }: { children: ReactNode }) => {
+  const [statusFilter, setStatusFilter] = useState<ProposalStatusFilter>(
     DEFAULT_STATUS_FILTER
   )
   const contextValue = useMemo(
     () => ({
       statusFilter,
-      setStatusFilter: (statusFilter: string) => {
+      setStatusFilter: (statusFilter: ProposalStatusFilter) => {
         setStatusFilter(statusFilter)
       },
     }),
     [statusFilter]
   )
-  return <StatusFilterContext.Provider {...props} value={contextValue} />
+  return (
+    <StatusFilterContext.Provider value={contextValue}>
+      {children}
+    </StatusFilterContext.Provider>
+  )
 }

+ 2 - 17
governance/xc_admin/packages/xc_admin_frontend/hooks/useMultisig.ts

@@ -5,24 +5,21 @@ import SquadsMesh from '@sqds/mesh'
 import { MultisigAccount, TransactionAccount } from '@sqds/mesh/lib/types'
 import { useCallback, useContext, useEffect, useMemo, useState } from 'react'
 import {
-  getMultisigCluster,
-  getProposals,
-  MultisigInstruction,
   PRICE_FEED_MULTISIG,
   UPGRADE_MULTISIG,
+  getMultisigCluster,
+  getProposals,
 } from 'xc_admin_common'
 import { ClusterContext } from '../contexts/ClusterContext'
 import { deriveWsUrl, pythClusterApiUrls } from '../utils/pythClusterApiUrl'
 
 export interface MultisigHookData {
   isLoading: boolean
-  error: any // TODO: fix any
   squads: SquadsMesh | undefined
   upgradeMultisigAccount: MultisigAccount | undefined
   priceFeedMultisigAccount: MultisigAccount | undefined
   upgradeMultisigProposals: TransactionAccount[]
   priceFeedMultisigProposals: TransactionAccount[]
-  allProposalsIxsParsed: MultisigInstruction[][]
   connection?: Connection
   refreshData?: () => { fetchData: () => Promise<void>; cancel: () => void }
 }
@@ -39,7 +36,6 @@ export const useMultisig = (): MultisigHookData => {
   const wallet = useAnchorWallet()
   const { cluster } = useContext(ClusterContext)
   const [isLoading, setIsLoading] = useState(true)
-  const [error, setError] = useState(null)
   const [upgradeMultisigAccount, setUpgradeMultisigAccount] =
     useState<MultisigAccount>()
   const [priceFeedMultisigAccount, setPriceFeedMultisigAccount] =
@@ -50,17 +46,10 @@ export const useMultisig = (): MultisigHookData => {
   const [priceFeedMultisigProposals, setPriceFeedMultisigProposals] = useState<
     TransactionAccount[]
   >([])
-  const [allProposalsIxsParsed, setAllProposalsIxsParsed] = useState<
-    MultisigInstruction[][]
-  >([])
   const [squads, setSquads] = useState<SquadsMesh | undefined>()
 
   const [urlsIndex, setUrlsIndex] = useState(0)
 
-  useEffect(() => {
-    setError(null)
-  }, [urlsIndex, cluster])
-
   useEffect(() => {
     setUrlsIndex(0)
   }, [cluster])
@@ -132,8 +121,6 @@ export const useMultisig = (): MultisigHookData => {
         if (cancelled) return
         const urls = pythClusterApiUrls(multisigCluster)
         if (urlsIndex === urls.length - 1) {
-          // @ts-ignore
-          setError(e)
           setIsLoading(false)
           console.warn(`Failed to fetch accounts`)
         } else if (urlsIndex < urls.length - 1) {
@@ -159,13 +146,11 @@ export const useMultisig = (): MultisigHookData => {
 
   return {
     isLoading,
-    error,
     squads,
     upgradeMultisigAccount,
     priceFeedMultisigAccount,
     upgradeMultisigProposals,
     priceFeedMultisigProposals,
-    allProposalsIxsParsed,
     refreshData,
     connection,
   }

+ 2 - 12
governance/xc_admin/packages/xc_admin_frontend/hooks/usePyth.ts

@@ -15,11 +15,8 @@ import { useContext, useEffect, useRef, useState } from 'react'
 import { ClusterContext } from '../contexts/ClusterContext'
 import { deriveWsUrl, pythClusterApiUrls } from '../utils/pythClusterApiUrl'
 
-const ONES = '11111111111111111111111111111111'
-
 interface PythHookData {
   isLoading: boolean
-  error: any // TODO: fix any
   rawConfig: RawConfig
   connection?: Connection
 }
@@ -47,17 +44,15 @@ export type PriceRawConfig = {
   publishers: PublicKey[]
 }
 
-const usePyth = (): PythHookData => {
+export const usePyth = (): PythHookData => {
   const connectionRef = useRef<Connection>()
   const { cluster } = useContext(ClusterContext)
   const [isLoading, setIsLoading] = useState(true)
-  const [error, setError] = useState(null)
   const [rawConfig, setRawConfig] = useState<RawConfig>({ mappingAccounts: [] })
   const [urlsIndex, setUrlsIndex] = useState(0)
 
   useEffect(() => {
     setIsLoading(true)
-    setError(null)
   }, [urlsIndex, cluster])
 
   useEffect(() => {
@@ -127,7 +122,7 @@ const usePyth = (): PythHookData => {
               } else {
                 let priceAccountKey: string | undefined =
                   parsed.priceAccountKey.toBase58()
-                let priceAccounts = []
+                const priceAccounts = []
                 while (priceAccountKey) {
                   const toAdd: PriceRawConfig = priceRawConfigs[priceAccountKey]
                   priceAccounts.push(toAdd)
@@ -196,8 +191,6 @@ const usePyth = (): PythHookData => {
       } catch (e) {
         if (cancelled) return
         if (urlsIndex === urls.length - 1) {
-          // @ts-ignore
-          setError(e)
           setIsLoading(false)
           console.warn(`Failed to fetch accounts`)
         } else if (urlsIndex < urls.length - 1) {
@@ -213,10 +206,7 @@ const usePyth = (): PythHookData => {
 
   return {
     isLoading,
-    error,
     connection: connectionRef.current,
     rawConfig,
   }
 }
-
-export default usePyth

+ 7 - 0
governance/xc_admin/packages/xc_admin_frontend/next.config.js

@@ -1,3 +1,5 @@
+const path = require('path')
+
 /** @type {import('next').NextConfig} */
 const nextConfig = {
   reactStrictMode: true,
@@ -16,6 +18,11 @@ const nextConfig = {
       loader: require.resolve('@svgr/webpack'),
     })
 
+    config.resolve.alias = {
+      ...config.resolve.alias,
+      '@images': path.resolve(__dirname, 'images/'),
+    }
+
     return config
   },
 }

+ 2 - 1
governance/xc_admin/packages/xc_admin_frontend/package.json

@@ -40,8 +40,9 @@
   },
   "devDependencies": {
     "@svgr/webpack": "^6.3.1",
+    "@typescript-eslint/eslint-plugin": "^7.7.0",
     "autoprefixer": "^10.4.8",
-    "eslint": "8.22.0",
+    "eslint": "8.56.0",
     "eslint-config-next": "12.2.5",
     "postcss": "^8.4.16",
     "prettier": "^2.7.1",

+ 1 - 1
governance/xc_admin/packages/xc_admin_frontend/pages/index.tsx

@@ -5,7 +5,7 @@ import { useRouter } from 'next/router'
 import { useEffect, useState } from 'react'
 import Layout from '../components/layout/Layout'
 import General from '../components/tabs/General'
-import Proposals from '../components/tabs/Proposals'
+import Proposals from '../components/tabs/Proposals/Proposals'
 import UpdatePermissions from '../components/tabs/UpdatePermissions'
 import { MultisigContextProvider } from '../contexts/MultisigContext'
 import { PythContextProvider } from '../contexts/PythContext'

+ 5 - 1
governance/xc_admin/packages/xc_admin_frontend/tsconfig.json

@@ -13,7 +13,11 @@
     "isolatedModules": true,
     "jsx": "preserve",
     "incremental": true,
-    "skipLibCheck": true
+    "skipLibCheck": true,
+    "paths": {
+      "@images/*": ["./images/*"],
+      "xc-admin-common": ["../xc_admin_common/src"]
+    }
   },
   "include": [
     "next-env.d.ts",

+ 527 - 199
package-lock.json

@@ -1459,26 +1459,6 @@
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
       }
     },
-    "express_relay/examples/easy_lend/node_modules/@humanwhocodes/config-array": {
-      "version": "0.11.14",
-      "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
-      "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
-      "dev": true,
-      "dependencies": {
-        "@humanwhocodes/object-schema": "^2.0.2",
-        "debug": "^4.3.1",
-        "minimatch": "^3.0.5"
-      },
-      "engines": {
-        "node": ">=10.10.0"
-      }
-    },
-    "express_relay/examples/easy_lend/node_modules/@humanwhocodes/object-schema": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz",
-      "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==",
-      "dev": true
-    },
     "express_relay/examples/easy_lend/node_modules/@noble/curves": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
@@ -1817,26 +1797,6 @@
         "url": "https://opencollective.com/eslint"
       }
     },
-    "express_relay/sdk/js/node_modules/@humanwhocodes/config-array": {
-      "version": "0.11.14",
-      "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
-      "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
-      "dev": true,
-      "dependencies": {
-        "@humanwhocodes/object-schema": "^2.0.2",
-        "debug": "^4.3.1",
-        "minimatch": "^3.0.5"
-      },
-      "engines": {
-        "node": ">=10.10.0"
-      }
-    },
-    "express_relay/sdk/js/node_modules/@humanwhocodes/object-schema": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz",
-      "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==",
-      "dev": true
-    },
     "express_relay/sdk/js/node_modules/@jest/console": {
       "version": "27.5.1",
       "resolved": "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz",
@@ -5069,8 +5029,9 @@
       },
       "devDependencies": {
         "@svgr/webpack": "^6.3.1",
+        "@typescript-eslint/eslint-plugin": "^7.7.0",
         "autoprefixer": "^10.4.8",
-        "eslint": "8.22.0",
+        "eslint": "8.56.0",
         "eslint-config-next": "12.2.5",
         "postcss": "^8.4.16",
         "prettier": "^2.7.1",
@@ -5133,6 +5094,257 @@
         "@types/node": "*"
       }
     },
+    "governance/xc_admin/packages/xc_admin_frontend/node_modules/@typescript-eslint/eslint-plugin": {
+      "version": "7.7.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.7.1.tgz",
+      "integrity": "sha512-KwfdWXJBOviaBVhxO3p5TJiLpNuh2iyXyjmWN0f1nU87pwyvfS0EmjC6ukQVYVFJd/K1+0NWGPDXiyEyQorn0Q==",
+      "dev": true,
+      "dependencies": {
+        "@eslint-community/regexpp": "^4.10.0",
+        "@typescript-eslint/scope-manager": "7.7.1",
+        "@typescript-eslint/type-utils": "7.7.1",
+        "@typescript-eslint/utils": "7.7.1",
+        "@typescript-eslint/visitor-keys": "7.7.1",
+        "debug": "^4.3.4",
+        "graphemer": "^1.4.0",
+        "ignore": "^5.3.1",
+        "natural-compare": "^1.4.0",
+        "semver": "^7.6.0",
+        "ts-api-utils": "^1.3.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || >=20.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "@typescript-eslint/parser": "^7.0.0",
+        "eslint": "^8.56.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "governance/xc_admin/packages/xc_admin_frontend/node_modules/@typescript-eslint/eslint-plugin/node_modules/ts-api-utils": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
+      "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=16"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.2.0"
+      }
+    },
+    "governance/xc_admin/packages/xc_admin_frontend/node_modules/@typescript-eslint/parser": {
+      "version": "7.7.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.7.1.tgz",
+      "integrity": "sha512-vmPzBOOtz48F6JAGVS/kZYk4EkXao6iGrD838sp1w3NQQC0W8ry/q641KU4PrG7AKNAf56NOcR8GOpH8l9FPCw==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "@typescript-eslint/scope-manager": "7.7.1",
+        "@typescript-eslint/types": "7.7.1",
+        "@typescript-eslint/typescript-estree": "7.7.1",
+        "@typescript-eslint/visitor-keys": "7.7.1",
+        "debug": "^4.3.4"
+      },
+      "engines": {
+        "node": "^18.18.0 || >=20.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^8.56.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "governance/xc_admin/packages/xc_admin_frontend/node_modules/@typescript-eslint/scope-manager": {
+      "version": "7.7.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.7.1.tgz",
+      "integrity": "sha512-PytBif2SF+9SpEUKynYn5g1RHFddJUcyynGpztX3l/ik7KmZEv19WCMhUBkHXPU9es/VWGD3/zg3wg90+Dh2rA==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/types": "7.7.1",
+        "@typescript-eslint/visitor-keys": "7.7.1"
+      },
+      "engines": {
+        "node": "^18.18.0 || >=20.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "governance/xc_admin/packages/xc_admin_frontend/node_modules/@typescript-eslint/type-utils": {
+      "version": "7.7.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.7.1.tgz",
+      "integrity": "sha512-ZksJLW3WF7o75zaBPScdW1Gbkwhd/lyeXGf1kQCxJaOeITscoSl0MjynVvCzuV5boUz/3fOI06Lz8La55mu29Q==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/typescript-estree": "7.7.1",
+        "@typescript-eslint/utils": "7.7.1",
+        "debug": "^4.3.4",
+        "ts-api-utils": "^1.3.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || >=20.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^8.56.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "governance/xc_admin/packages/xc_admin_frontend/node_modules/@typescript-eslint/type-utils/node_modules/ts-api-utils": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
+      "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=16"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.2.0"
+      }
+    },
+    "governance/xc_admin/packages/xc_admin_frontend/node_modules/@typescript-eslint/types": {
+      "version": "7.7.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.7.1.tgz",
+      "integrity": "sha512-AmPmnGW1ZLTpWa+/2omPrPfR7BcbUU4oha5VIbSbS1a1Tv966bklvLNXxp3mrbc+P2j4MNOTfDffNsk4o0c6/w==",
+      "dev": true,
+      "engines": {
+        "node": "^18.18.0 || >=20.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "governance/xc_admin/packages/xc_admin_frontend/node_modules/@typescript-eslint/typescript-estree": {
+      "version": "7.7.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.7.1.tgz",
+      "integrity": "sha512-CXe0JHCXru8Fa36dteXqmH2YxngKJjkQLjxzoj6LYwzZ7qZvgsLSc+eqItCrqIop8Vl2UKoAi0StVWu97FQZIQ==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/types": "7.7.1",
+        "@typescript-eslint/visitor-keys": "7.7.1",
+        "debug": "^4.3.4",
+        "globby": "^11.1.0",
+        "is-glob": "^4.0.3",
+        "minimatch": "^9.0.4",
+        "semver": "^7.6.0",
+        "ts-api-utils": "^1.3.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || >=20.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "governance/xc_admin/packages/xc_admin_frontend/node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+      "version": "9.0.4",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
+      "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
+      "dev": true,
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "governance/xc_admin/packages/xc_admin_frontend/node_modules/@typescript-eslint/typescript-estree/node_modules/ts-api-utils": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
+      "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=16"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.2.0"
+      }
+    },
+    "governance/xc_admin/packages/xc_admin_frontend/node_modules/@typescript-eslint/utils": {
+      "version": "7.7.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.7.1.tgz",
+      "integrity": "sha512-QUvBxPEaBXf41ZBbaidKICgVL8Hin0p6prQDu6bbetWo39BKbWJxRsErOzMNT1rXvTll+J7ChrbmMCXM9rsvOQ==",
+      "dev": true,
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.4.0",
+        "@types/json-schema": "^7.0.15",
+        "@types/semver": "^7.5.8",
+        "@typescript-eslint/scope-manager": "7.7.1",
+        "@typescript-eslint/types": "7.7.1",
+        "@typescript-eslint/typescript-estree": "7.7.1",
+        "semver": "^7.6.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || >=20.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^8.56.0"
+      }
+    },
+    "governance/xc_admin/packages/xc_admin_frontend/node_modules/@typescript-eslint/visitor-keys": {
+      "version": "7.7.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.7.1.tgz",
+      "integrity": "sha512-gBL3Eq25uADw1LQ9kVpf3hRM+DWzs0uZknHYK3hq4jcTPqVCClHGDnB6UUUV2SFeBeA4KWHWbbLqmbGcZ4FYbw==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/types": "7.7.1",
+        "eslint-visitor-keys": "^3.4.3"
+      },
+      "engines": {
+        "node": "^18.18.0 || >=20.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "governance/xc_admin/packages/xc_admin_frontend/node_modules/brace-expansion": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+      "dev": true,
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
     "governance/xc_admin/packages/xc_admin_frontend/node_modules/cross-fetch": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz",
@@ -5154,50 +5366,49 @@
       }
     },
     "governance/xc_admin/packages/xc_admin_frontend/node_modules/eslint": {
-      "version": "8.22.0",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.22.0.tgz",
-      "integrity": "sha512-ci4t0sz6vSRKdmkOGmprBo6fmI4PrphDFMy5JEq/fNS0gQkJM3rLmrqcp8ipMcdobH3KtUP40KniAE9W19S4wA==",
+      "version": "8.56.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz",
+      "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==",
       "dev": true,
       "dependencies": {
-        "@eslint/eslintrc": "^1.3.0",
-        "@humanwhocodes/config-array": "^0.10.4",
-        "@humanwhocodes/gitignore-to-minimatch": "^1.0.2",
-        "ajv": "^6.10.0",
+        "@eslint-community/eslint-utils": "^4.2.0",
+        "@eslint-community/regexpp": "^4.6.1",
+        "@eslint/eslintrc": "^2.1.4",
+        "@eslint/js": "8.56.0",
+        "@humanwhocodes/config-array": "^0.11.13",
+        "@humanwhocodes/module-importer": "^1.0.1",
+        "@nodelib/fs.walk": "^1.2.8",
+        "@ungap/structured-clone": "^1.2.0",
+        "ajv": "^6.12.4",
         "chalk": "^4.0.0",
         "cross-spawn": "^7.0.2",
         "debug": "^4.3.2",
         "doctrine": "^3.0.0",
         "escape-string-regexp": "^4.0.0",
-        "eslint-scope": "^7.1.1",
-        "eslint-utils": "^3.0.0",
-        "eslint-visitor-keys": "^3.3.0",
-        "espree": "^9.3.3",
-        "esquery": "^1.4.0",
+        "eslint-scope": "^7.2.2",
+        "eslint-visitor-keys": "^3.4.3",
+        "espree": "^9.6.1",
+        "esquery": "^1.4.2",
         "esutils": "^2.0.2",
         "fast-deep-equal": "^3.1.3",
         "file-entry-cache": "^6.0.1",
         "find-up": "^5.0.0",
-        "functional-red-black-tree": "^1.0.1",
-        "glob-parent": "^6.0.1",
-        "globals": "^13.15.0",
-        "globby": "^11.1.0",
-        "grapheme-splitter": "^1.0.4",
+        "glob-parent": "^6.0.2",
+        "globals": "^13.19.0",
+        "graphemer": "^1.4.0",
         "ignore": "^5.2.0",
-        "import-fresh": "^3.0.0",
         "imurmurhash": "^0.1.4",
         "is-glob": "^4.0.0",
+        "is-path-inside": "^3.0.3",
         "js-yaml": "^4.1.0",
         "json-stable-stringify-without-jsonify": "^1.0.1",
         "levn": "^0.4.1",
         "lodash.merge": "^4.6.2",
         "minimatch": "^3.1.2",
         "natural-compare": "^1.4.0",
-        "optionator": "^0.9.1",
-        "regexpp": "^3.2.0",
+        "optionator": "^0.9.3",
         "strip-ansi": "^6.0.1",
-        "strip-json-comments": "^3.1.0",
-        "text-table": "^0.2.0",
-        "v8-compile-cache": "^2.0.3"
+        "text-table": "^0.2.0"
       },
       "bin": {
         "eslint": "bin/eslint.js"
@@ -5276,6 +5487,18 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "governance/xc_admin/packages/xc_admin_frontend/node_modules/lru-cache": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+      "dev": true,
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "governance/xc_admin/packages/xc_admin_frontend/node_modules/p-limit": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@@ -5306,6 +5529,21 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "governance/xc_admin/packages/xc_admin_frontend/node_modules/semver": {
+      "version": "7.6.0",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
+      "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
+      "dev": true,
+      "dependencies": {
+        "lru-cache": "^6.0.0"
+      },
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "governance/xc_admin/packages/xc_admin_frontend/node_modules/web3": {
       "version": "4.8.0",
       "resolved": "https://registry.npmjs.org/web3/-/web3-4.8.0.tgz",
@@ -10047,29 +10285,18 @@
       }
     },
     "node_modules/@humanwhocodes/config-array": {
-      "version": "0.10.7",
-      "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.7.tgz",
-      "integrity": "sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w==",
-      "dev": true,
+      "version": "0.11.14",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
+      "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
       "dependencies": {
-        "@humanwhocodes/object-schema": "^1.2.1",
-        "debug": "^4.1.1",
-        "minimatch": "^3.0.4"
+        "@humanwhocodes/object-schema": "^2.0.2",
+        "debug": "^4.3.1",
+        "minimatch": "^3.0.5"
       },
       "engines": {
         "node": ">=10.10.0"
       }
     },
-    "node_modules/@humanwhocodes/gitignore-to-minimatch": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz",
-      "integrity": "sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==",
-      "dev": true,
-      "funding": {
-        "type": "github",
-        "url": "https://github.com/sponsors/nzakas"
-      }
-    },
     "node_modules/@humanwhocodes/module-importer": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
@@ -10083,9 +10310,9 @@
       }
     },
     "node_modules/@humanwhocodes/object-schema": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
-      "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA=="
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
+      "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="
     },
     "node_modules/@hutson/parse-repository-url": {
       "version": "3.0.2",
@@ -22040,9 +22267,9 @@
       }
     },
     "node_modules/@types/json-schema": {
-      "version": "7.0.11",
-      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
-      "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ=="
+      "version": "7.0.15",
+      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+      "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="
     },
     "node_modules/@types/json5": {
       "version": "0.0.29",
@@ -22235,9 +22462,9 @@
       }
     },
     "node_modules/@types/semver": {
-      "version": "7.3.13",
-      "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz",
-      "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw=="
+      "version": "7.5.8",
+      "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
+      "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ=="
     },
     "node_modules/@types/serve-index": {
       "version": "1.9.1",
@@ -30391,19 +30618,6 @@
         "url": "https://github.com/chalk/supports-color?sponsor=1"
       }
     },
-    "node_modules/eslint/node_modules/@humanwhocodes/config-array": {
-      "version": "0.11.8",
-      "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
-      "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==",
-      "dependencies": {
-        "@humanwhocodes/object-schema": "^1.2.1",
-        "debug": "^4.1.1",
-        "minimatch": "^3.0.5"
-      },
-      "engines": {
-        "node": ">=10.10.0"
-      }
-    },
     "node_modules/eslint/node_modules/escape-string-regexp": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@@ -35004,9 +35218,9 @@
       ]
     },
     "node_modules/ignore": {
-      "version": "5.2.4",
-      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
-      "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
+      "version": "5.3.1",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
+      "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==",
       "engines": {
         "node": ">= 4"
       }
@@ -64059,31 +64273,24 @@
       }
     },
     "@humanwhocodes/config-array": {
-      "version": "0.10.7",
-      "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.7.tgz",
-      "integrity": "sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w==",
-      "dev": true,
+      "version": "0.11.14",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
+      "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
       "requires": {
-        "@humanwhocodes/object-schema": "^1.2.1",
-        "debug": "^4.1.1",
-        "minimatch": "^3.0.4"
+        "@humanwhocodes/object-schema": "^2.0.2",
+        "debug": "^4.3.1",
+        "minimatch": "^3.0.5"
       }
     },
-    "@humanwhocodes/gitignore-to-minimatch": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz",
-      "integrity": "sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==",
-      "dev": true
-    },
     "@humanwhocodes/module-importer": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
       "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="
     },
     "@humanwhocodes/object-schema": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
-      "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA=="
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
+      "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="
     },
     "@hutson/parse-repository-url": {
       "version": "3.0.2",
@@ -68614,23 +68821,6 @@
             "strip-json-comments": "^3.1.1"
           }
         },
-        "@humanwhocodes/config-array": {
-          "version": "0.11.14",
-          "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
-          "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
-          "dev": true,
-          "requires": {
-            "@humanwhocodes/object-schema": "^2.0.2",
-            "debug": "^4.3.1",
-            "minimatch": "^3.0.5"
-          }
-        },
-        "@humanwhocodes/object-schema": {
-          "version": "2.0.2",
-          "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz",
-          "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==",
-          "dev": true
-        },
         "@jest/console": {
           "version": "27.5.1",
           "resolved": "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz",
@@ -77796,9 +77986,9 @@
       }
     },
     "@types/json-schema": {
-      "version": "7.0.11",
-      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
-      "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ=="
+      "version": "7.0.15",
+      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+      "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="
     },
     "@types/json5": {
       "version": "0.0.29",
@@ -77990,9 +78180,9 @@
       }
     },
     "@types/semver": {
-      "version": "7.3.13",
-      "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz",
-      "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw=="
+      "version": "7.5.8",
+      "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
+      "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ=="
     },
     "@types/serve-index": {
       "version": "1.9.1",
@@ -84064,23 +84254,6 @@
           "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==",
           "dev": true
         },
-        "@humanwhocodes/config-array": {
-          "version": "0.11.14",
-          "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
-          "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
-          "dev": true,
-          "requires": {
-            "@humanwhocodes/object-schema": "^2.0.2",
-            "debug": "^4.3.1",
-            "minimatch": "^3.0.5"
-          }
-        },
-        "@humanwhocodes/object-schema": {
-          "version": "2.0.2",
-          "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz",
-          "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==",
-          "dev": true
-        },
         "@noble/curves": {
           "version": "1.2.0",
           "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
@@ -84797,16 +84970,6 @@
         "text-table": "^0.2.0"
       },
       "dependencies": {
-        "@humanwhocodes/config-array": {
-          "version": "0.11.8",
-          "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
-          "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==",
-          "requires": {
-            "@humanwhocodes/object-schema": "^1.2.1",
-            "debug": "^4.1.1",
-            "minimatch": "^3.0.5"
-          }
-        },
         "escape-string-regexp": {
           "version": "4.0.0",
           "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@@ -88921,9 +89084,9 @@
       "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
     },
     "ignore": {
-      "version": "5.2.4",
-      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
-      "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ=="
+      "version": "5.3.1",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
+      "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw=="
     },
     "ignore-walk": {
       "version": "5.0.1",
@@ -107414,10 +107577,11 @@
         "@types/node": "^18.11.18",
         "@types/react": "18.0.26",
         "@types/react-dom": "18.0.10",
+        "@typescript-eslint/eslint-plugin": "^7.7.0",
         "autoprefixer": "^10.4.8",
         "axios": "^1.4.0",
         "copy-to-clipboard": "^3.3.3",
-        "eslint": "8.22.0",
+        "eslint": "8.56.0",
         "eslint-config-next": "12.2.5",
         "gsap": "^3.11.4",
         "next": "12.2.5",
@@ -107475,6 +107639,153 @@
             "@types/node": "*"
           }
         },
+        "@typescript-eslint/eslint-plugin": {
+          "version": "7.7.1",
+          "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.7.1.tgz",
+          "integrity": "sha512-KwfdWXJBOviaBVhxO3p5TJiLpNuh2iyXyjmWN0f1nU87pwyvfS0EmjC6ukQVYVFJd/K1+0NWGPDXiyEyQorn0Q==",
+          "dev": true,
+          "requires": {
+            "@eslint-community/regexpp": "^4.10.0",
+            "@typescript-eslint/scope-manager": "7.7.1",
+            "@typescript-eslint/type-utils": "7.7.1",
+            "@typescript-eslint/utils": "7.7.1",
+            "@typescript-eslint/visitor-keys": "7.7.1",
+            "debug": "^4.3.4",
+            "graphemer": "^1.4.0",
+            "ignore": "^5.3.1",
+            "natural-compare": "^1.4.0",
+            "semver": "^7.6.0",
+            "ts-api-utils": "^1.3.0"
+          },
+          "dependencies": {
+            "ts-api-utils": {
+              "version": "1.3.0",
+              "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
+              "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==",
+              "dev": true,
+              "requires": {}
+            }
+          }
+        },
+        "@typescript-eslint/parser": {
+          "version": "7.7.1",
+          "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.7.1.tgz",
+          "integrity": "sha512-vmPzBOOtz48F6JAGVS/kZYk4EkXao6iGrD838sp1w3NQQC0W8ry/q641KU4PrG7AKNAf56NOcR8GOpH8l9FPCw==",
+          "dev": true,
+          "peer": true,
+          "requires": {
+            "@typescript-eslint/scope-manager": "7.7.1",
+            "@typescript-eslint/types": "7.7.1",
+            "@typescript-eslint/typescript-estree": "7.7.1",
+            "@typescript-eslint/visitor-keys": "7.7.1",
+            "debug": "^4.3.4"
+          }
+        },
+        "@typescript-eslint/scope-manager": {
+          "version": "7.7.1",
+          "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.7.1.tgz",
+          "integrity": "sha512-PytBif2SF+9SpEUKynYn5g1RHFddJUcyynGpztX3l/ik7KmZEv19WCMhUBkHXPU9es/VWGD3/zg3wg90+Dh2rA==",
+          "dev": true,
+          "requires": {
+            "@typescript-eslint/types": "7.7.1",
+            "@typescript-eslint/visitor-keys": "7.7.1"
+          }
+        },
+        "@typescript-eslint/type-utils": {
+          "version": "7.7.1",
+          "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.7.1.tgz",
+          "integrity": "sha512-ZksJLW3WF7o75zaBPScdW1Gbkwhd/lyeXGf1kQCxJaOeITscoSl0MjynVvCzuV5boUz/3fOI06Lz8La55mu29Q==",
+          "dev": true,
+          "requires": {
+            "@typescript-eslint/typescript-estree": "7.7.1",
+            "@typescript-eslint/utils": "7.7.1",
+            "debug": "^4.3.4",
+            "ts-api-utils": "^1.3.0"
+          },
+          "dependencies": {
+            "ts-api-utils": {
+              "version": "1.3.0",
+              "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
+              "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==",
+              "dev": true,
+              "requires": {}
+            }
+          }
+        },
+        "@typescript-eslint/types": {
+          "version": "7.7.1",
+          "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.7.1.tgz",
+          "integrity": "sha512-AmPmnGW1ZLTpWa+/2omPrPfR7BcbUU4oha5VIbSbS1a1Tv966bklvLNXxp3mrbc+P2j4MNOTfDffNsk4o0c6/w==",
+          "dev": true
+        },
+        "@typescript-eslint/typescript-estree": {
+          "version": "7.7.1",
+          "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.7.1.tgz",
+          "integrity": "sha512-CXe0JHCXru8Fa36dteXqmH2YxngKJjkQLjxzoj6LYwzZ7qZvgsLSc+eqItCrqIop8Vl2UKoAi0StVWu97FQZIQ==",
+          "dev": true,
+          "requires": {
+            "@typescript-eslint/types": "7.7.1",
+            "@typescript-eslint/visitor-keys": "7.7.1",
+            "debug": "^4.3.4",
+            "globby": "^11.1.0",
+            "is-glob": "^4.0.3",
+            "minimatch": "^9.0.4",
+            "semver": "^7.6.0",
+            "ts-api-utils": "^1.3.0"
+          },
+          "dependencies": {
+            "minimatch": {
+              "version": "9.0.4",
+              "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
+              "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
+              "dev": true,
+              "requires": {
+                "brace-expansion": "^2.0.1"
+              }
+            },
+            "ts-api-utils": {
+              "version": "1.3.0",
+              "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
+              "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==",
+              "dev": true,
+              "requires": {}
+            }
+          }
+        },
+        "@typescript-eslint/utils": {
+          "version": "7.7.1",
+          "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.7.1.tgz",
+          "integrity": "sha512-QUvBxPEaBXf41ZBbaidKICgVL8Hin0p6prQDu6bbetWo39BKbWJxRsErOzMNT1rXvTll+J7ChrbmMCXM9rsvOQ==",
+          "dev": true,
+          "requires": {
+            "@eslint-community/eslint-utils": "^4.4.0",
+            "@types/json-schema": "^7.0.15",
+            "@types/semver": "^7.5.8",
+            "@typescript-eslint/scope-manager": "7.7.1",
+            "@typescript-eslint/types": "7.7.1",
+            "@typescript-eslint/typescript-estree": "7.7.1",
+            "semver": "^7.6.0"
+          }
+        },
+        "@typescript-eslint/visitor-keys": {
+          "version": "7.7.1",
+          "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.7.1.tgz",
+          "integrity": "sha512-gBL3Eq25uADw1LQ9kVpf3hRM+DWzs0uZknHYK3hq4jcTPqVCClHGDnB6UUUV2SFeBeA4KWHWbbLqmbGcZ4FYbw==",
+          "dev": true,
+          "requires": {
+            "@typescript-eslint/types": "7.7.1",
+            "eslint-visitor-keys": "^3.4.3"
+          }
+        },
+        "brace-expansion": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+          "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+          "dev": true,
+          "requires": {
+            "balanced-match": "^1.0.0"
+          }
+        },
         "cross-fetch": {
           "version": "4.0.0",
           "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz",
@@ -107490,50 +107801,49 @@
           "dev": true
         },
         "eslint": {
-          "version": "8.22.0",
-          "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.22.0.tgz",
-          "integrity": "sha512-ci4t0sz6vSRKdmkOGmprBo6fmI4PrphDFMy5JEq/fNS0gQkJM3rLmrqcp8ipMcdobH3KtUP40KniAE9W19S4wA==",
+          "version": "8.56.0",
+          "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz",
+          "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==",
           "dev": true,
           "requires": {
-            "@eslint/eslintrc": "^1.3.0",
-            "@humanwhocodes/config-array": "^0.10.4",
-            "@humanwhocodes/gitignore-to-minimatch": "^1.0.2",
-            "ajv": "^6.10.0",
+            "@eslint-community/eslint-utils": "^4.2.0",
+            "@eslint-community/regexpp": "^4.6.1",
+            "@eslint/eslintrc": "^2.1.4",
+            "@eslint/js": "8.56.0",
+            "@humanwhocodes/config-array": "^0.11.13",
+            "@humanwhocodes/module-importer": "^1.0.1",
+            "@nodelib/fs.walk": "^1.2.8",
+            "@ungap/structured-clone": "^1.2.0",
+            "ajv": "^6.12.4",
             "chalk": "^4.0.0",
             "cross-spawn": "^7.0.2",
             "debug": "^4.3.2",
             "doctrine": "^3.0.0",
             "escape-string-regexp": "^4.0.0",
-            "eslint-scope": "^7.1.1",
-            "eslint-utils": "^3.0.0",
-            "eslint-visitor-keys": "^3.3.0",
-            "espree": "^9.3.3",
-            "esquery": "^1.4.0",
+            "eslint-scope": "^7.2.2",
+            "eslint-visitor-keys": "^3.4.3",
+            "espree": "^9.6.1",
+            "esquery": "^1.4.2",
             "esutils": "^2.0.2",
             "fast-deep-equal": "^3.1.3",
             "file-entry-cache": "^6.0.1",
             "find-up": "^5.0.0",
-            "functional-red-black-tree": "^1.0.1",
-            "glob-parent": "^6.0.1",
-            "globals": "^13.15.0",
-            "globby": "^11.1.0",
-            "grapheme-splitter": "^1.0.4",
+            "glob-parent": "^6.0.2",
+            "globals": "^13.19.0",
+            "graphemer": "^1.4.0",
             "ignore": "^5.2.0",
-            "import-fresh": "^3.0.0",
             "imurmurhash": "^0.1.4",
             "is-glob": "^4.0.0",
+            "is-path-inside": "^3.0.3",
             "js-yaml": "^4.1.0",
             "json-stable-stringify-without-jsonify": "^1.0.1",
             "levn": "^0.4.1",
             "lodash.merge": "^4.6.2",
             "minimatch": "^3.1.2",
             "natural-compare": "^1.4.0",
-            "optionator": "^0.9.1",
-            "regexpp": "^3.2.0",
+            "optionator": "^0.9.3",
             "strip-ansi": "^6.0.1",
-            "strip-json-comments": "^3.1.0",
-            "text-table": "^0.2.0",
-            "v8-compile-cache": "^2.0.3"
+            "text-table": "^0.2.0"
           }
         },
         "ethereum-cryptography": {
@@ -107586,6 +107896,15 @@
             "p-locate": "^5.0.0"
           }
         },
+        "lru-cache": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+          "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+          "dev": true,
+          "requires": {
+            "yallist": "^4.0.0"
+          }
+        },
         "p-limit": {
           "version": "3.1.0",
           "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@@ -107604,6 +107923,15 @@
             "p-limit": "^3.0.2"
           }
         },
+        "semver": {
+          "version": "7.6.0",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
+          "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
+          "dev": true,
+          "requires": {
+            "lru-cache": "^6.0.0"
+          }
+        },
         "web3": {
           "version": "4.8.0",
           "resolved": "https://registry.npmjs.org/web3/-/web3-4.8.0.tgz",