Parcourir la source

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

* fetch all proposals instructions and pass to individual proposal row

* add new verified icon

* enable scroll to top when clicking on individual proposal

* split devnet/pythtest and mainnet-beta/pythnet proposals

* show symbols for product and price account when possible

* fix typo

* move getAllIxs to hook and remove verified for draft proposals
Daniel Chew il y a 2 ans
Parent
commit
b2cae745c8

+ 232 - 163
governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals.tsx

@@ -1,8 +1,6 @@
-import { BN } from '@coral-xyz/anchor'
+import * as Tooltip from '@radix-ui/react-tooltip'
 import { AccountMeta, PublicKey } from '@solana/web3.js'
-import { getIxPDA } from '@sqds/mesh'
 import { MultisigAccount, TransactionAccount } from '@sqds/mesh/lib/types'
-import copy from 'copy-to-clipboard'
 import { useRouter } from 'next/router'
 import {
   Dispatch,
@@ -15,7 +13,6 @@ import {
 import toast from 'react-hot-toast'
 import {
   ExecutePostedVaa,
-  getMultisigCluster,
   getRemoteCluster,
   MultisigInstruction,
   MultisigParser,
@@ -25,7 +22,8 @@ import {
 } from 'xc_admin_common'
 import { ClusterContext } from '../../contexts/ClusterContext'
 import { useMultisigContext } from '../../contexts/MultisigContext'
-import CopyIcon from '../../images/icons/copy.inline.svg'
+import { usePythContext } from '../../contexts/PythContext'
+import VerifiedIcon from '../../images/icons/verified.inline.svg'
 import { capitalizeFirstLetter } from '../../utils/capitalizeFirstLetter'
 import ClusterSwitch from '../ClusterSwitch'
 import CopyPubkey from '../common/CopyPubkey'
@@ -43,9 +41,11 @@ const isPubkey = (str: string) => {
 
 const ProposalRow = ({
   proposal,
+  verified,
   setCurrentProposalPubkey,
 }: {
   proposal: TransactionAccount
+  verified: boolean
   setCurrentProposalPubkey: Dispatch<SetStateAction<string | undefined>>
 }) => {
   const status = Object.keys(proposal.status)[0]
@@ -62,7 +62,7 @@ const ProposalRow = ({
           query: router.query,
         },
         undefined,
-        { scroll: false }
+        { scroll: true }
       )
     },
     [setCurrentProposalPubkey, router]
@@ -75,8 +75,7 @@ const ProposalRow = ({
       }
     >
       <div className="flex justify-between p-4">
-        <div>
-          {' '}
+        <div className="flex">
           <span className="mr-2 hidden sm:block">
             {proposal.publicKey.toBase58()}
           </span>
@@ -85,9 +84,11 @@ const ProposalRow = ({
               '...' +
               proposal.publicKey.toBase58().slice(-6)}
           </span>{' '}
+          {verified ? <VerifiedIconWithTooltip /> : null}
+        </div>
+        <div>
+          <StatusTag proposalStatus={status} />
         </div>
-
-        <StatusTag proposalStatus={status} />
       </div>
     </div>
   )
@@ -129,82 +130,86 @@ const StatusTag = ({ proposalStatus }: { proposalStatus: string }) => {
   )
 }
 
+const VerifiedIconWithTooltip = () => {
+  return (
+    <div className="flex items-center">
+      <Tooltip.Provider delayDuration={100} skipDelayDuration={500}>
+        <Tooltip.Root>
+          <Tooltip.Trigger>
+            <VerifiedIcon />
+          </Tooltip.Trigger>
+          <Tooltip.Content side="top" sideOffset={8}>
+            <span className="inline-block bg-darkGray3 p-2 text-xs text-light hoverable:bg-darkGray">
+              The instructions in this proposal are verified.
+            </span>
+          </Tooltip.Content>
+        </Tooltip.Root>
+      </Tooltip.Provider>
+    </div>
+  )
+}
+
+const ParsedAccountPubkeyRow = ({
+  mapping,
+  title,
+  pubkey,
+}: {
+  mapping: { [key: string]: string }
+  title: string
+  pubkey: string
+}) => {
+  return (
+    <div className="flex justify-between pb-3">
+      <div className="max-w-[80px] break-words sm:max-w-none sm:break-normal">
+        &#10551; {title}
+      </div>
+      <div className="space-y-2 sm:flex sm:space-x-2">{mapping[pubkey]}</div>
+    </div>
+  )
+}
+
 const Proposal = ({
   proposal,
+  instructions,
+  verified,
   multisig,
 }: {
   proposal: TransactionAccount | undefined
+  instructions: MultisigInstruction[]
+  verified: boolean
   multisig: MultisigAccount | undefined
 }) => {
-  const [proposalInstructions, setProposalInstructions] = useState<
-    MultisigInstruction[]
-  >([])
-  const [isProposalInstructionsLoading, setIsProposalInstructionsLoading] =
-    useState(false)
-  const [isVerified, setIsVerified] = useState(false)
+  const [
+    productAccountKeyToSymbolMapping,
+    setProductAccountKeyToSymbolMapping,
+  ] = useState<{ [key: string]: string }>({})
+  const [priceAccountKeyToSymbolMapping, setPriceAccountKeyToSymbolMapping] =
+    useState<{ [key: string]: string }>({})
   const { cluster } = useContext(ClusterContext)
   const { squads, isLoading: isMultisigLoading } = useMultisigContext()
-
-  const proposalStatus = proposal ? Object.keys(proposal.status)[0] : 'unknown'
+  const { rawConfig, dataIsLoading, connection } = usePythContext()
 
   useEffect(() => {
-    const fetchProposalInstructions = async () => {
-      const multisigParser = MultisigParser.fromCluster(
-        getMultisigCluster(cluster)
+    if (!dataIsLoading) {
+      const productAccountMapping: { [key: string]: string } = {}
+      const priceAccountMapping: { [key: string]: string } = {}
+      rawConfig.mappingAccounts.map((acc) =>
+        acc.products.map((prod) => {
+          productAccountMapping[prod.address.toBase58()] = prod.metadata.symbol
+          priceAccountMapping[prod.priceAccounts[0].address.toBase58()] =
+            prod.metadata.symbol
+        })
       )
-      if (squads && proposal) {
-        setIsProposalInstructionsLoading(true)
-        const proposalIxs = []
-        for (let i = 1; i <= proposal.instructionIndex; i++) {
-          const instructionPda = getIxPDA(
-            proposal.publicKey,
-            new BN(i),
-            squads.multisigProgramId
-          )[0]
-          const instruction = await squads.getInstruction(instructionPda)
-          const parsedInstruction = multisigParser.parseInstruction({
-            programId: instruction.programId,
-            data: instruction.data as Buffer,
-            keys: instruction.keys as AccountMeta[],
-          })
-          proposalIxs.push(parsedInstruction)
-        }
-        setIsVerified(
-          proposalIxs.every(
-            (ix) =>
-              ix instanceof PythMultisigInstruction ||
-              (ix instanceof WormholeMultisigInstruction &&
-                ix.name === 'postMessage' &&
-                ix.governanceAction instanceof ExecutePostedVaa &&
-                ix.governanceAction.instructions.every((remoteIx) => {
-                  const innerMultisigParser = MultisigParser.fromCluster(
-                    getRemoteCluster(cluster)
-                  )
-                  const parsedRemoteInstruction =
-                    innerMultisigParser.parseInstruction({
-                      programId: remoteIx.programId,
-                      data: remoteIx.data as Buffer,
-                      keys: remoteIx.keys as AccountMeta[],
-                    })
-                  return (
-                    parsedRemoteInstruction instanceof PythMultisigInstruction
-                  )
-                }) &&
-                ix.governanceAction.targetChainId === 'pythnet')
-          )
-        )
-        setProposalInstructions(proposalIxs)
-        setIsProposalInstructionsLoading(false)
-      }
+      setProductAccountKeyToSymbolMapping(productAccountMapping)
+      setPriceAccountKeyToSymbolMapping(priceAccountMapping)
     }
+  }, [rawConfig, dataIsLoading])
 
-    fetchProposalInstructions()
-  }, [proposal, squads, cluster])
+  const proposalStatus = proposal ? Object.keys(proposal.status)[0] : 'unknown'
 
   const handleClickApprove = async () => {
     if (proposal && squads) {
       try {
-        console.log(squads.wallet.publicKey.toBase58())
         await squads.approveTransaction(proposal.publicKey)
         toast.success(`Approved proposal ${proposal.publicKey.toBase58()}`)
       } catch (e: any) {
@@ -248,19 +253,12 @@ const Proposal = ({
 
   return proposal !== undefined &&
     multisig !== undefined &&
-    !isMultisigLoading &&
-    !isProposalInstructionsLoading ? (
+    !isMultisigLoading ? (
     <div className="grid grid-cols-3 gap-4">
       <div className="col-span-3 my-2 space-y-4 bg-[#1E1B2F] p-4 lg:col-span-2">
         <div className="flex justify-between">
           <h4 className="h4 font-semibold">Info</h4>
-          <div
-            className={`flex items-center justify-center rounded-full py-1 px-2 text-xs ${
-              isVerified ? 'bg-[#187B51]' : 'bg-[#8D2D41]'
-            }`}
-          >
-            {isVerified ? 'Verified' : 'Unverified'}
-          </div>
+          {verified ? <VerifiedIconWithTooltip /> : null}
         </div>
         <hr className="border-gray-700" />
         <div className="flex justify-between">
@@ -339,9 +337,11 @@ const Proposal = ({
         ) : null}
       </div>
       <div className="col-span-3 my-2 space-y-4 bg-[#1E1B2F] p-4">
-        <h4 className="h4 font-semibold">Instructions</h4>
+        <h4 className="h4 font-semibold">
+          Total Instructions: {instructions.length}
+        </h4>
         <hr className="border-gray-700" />
-        {proposalInstructions?.map((instruction, index) => (
+        {instructions?.map((instruction, index) => (
           <>
             <h4 className="h4 text-[20px] font-semibold">
               Instruction {index + 1}
@@ -455,8 +455,8 @@ const Proposal = ({
                             <div className="max-w-[80px] break-words sm:max-w-none sm:break-normal">
                               {key}
                             </div>
-                            <div className="space-y-2 sm:flex sm:space-x-2">
-                              <div className="flex items-center space-x-2 sm:mt-2 sm:ml-2">
+                            <div className="space-y-2 sm:flex sm:space-y-0 sm:space-x-2">
+                              <div className="flex items-center space-x-2 sm:ml-2">
                                 {instruction.accounts.named[key].isSigner ? (
                                   <SignerTag />
                                 ) : null}
@@ -471,6 +471,29 @@ const Proposal = ({
                               />
                             </div>
                           </div>
+                          {key === 'priceAccount' &&
+                          instruction.accounts.named[key].pubkey.toBase58() in
+                            priceAccountKeyToSymbolMapping ? (
+                            <ParsedAccountPubkeyRow
+                              key="priceAccountPubkey"
+                              mapping={priceAccountKeyToSymbolMapping}
+                              title="symbol"
+                              pubkey={instruction.accounts.named[
+                                key
+                              ].pubkey.toBase58()}
+                            />
+                          ) : key === 'productAccount' &&
+                            instruction.accounts.named[key].pubkey.toBase58() in
+                              productAccountKeyToSymbolMapping ? (
+                            <ParsedAccountPubkeyRow
+                              key="productAccountPubkey"
+                              mapping={productAccountKeyToSymbolMapping}
+                              title="symbol"
+                              pubkey={instruction.accounts.named[
+                                key
+                              ].pubkey.toBase58()}
+                            />
+                          ) : null}
                         </>
                       )
                     )}
@@ -518,22 +541,7 @@ const Proposal = ({
                           <div className="flex space-x-2">
                             {key.isSigner ? <SignerTag /> : null}
                             {key.isWritable ? <WritableTag /> : null}
-                            <div
-                              className="-ml-1 inline-flex cursor-pointer items-center px-1 hover:bg-dark hover:text-white active:bg-darkGray3"
-                              onClick={() => {
-                                copy(key.pubkey.toBase58())
-                              }}
-                            >
-                              <span className="mr-2 hidden xl:block">
-                                {key.pubkey.toBase58()}
-                              </span>
-                              <span className="mr-2 xl:hidden">
-                                {key.pubkey.toBase58().slice(0, 6) +
-                                  '...' +
-                                  key.pubkey.toBase58().slice(-6)}
-                              </span>{' '}
-                              <CopyIcon className="shrink-0" />
-                            </div>
+                            <CopyPubkey pubkey={key.pubkey.toBase58()} />
                           </div>
                         </div>
                       </>
@@ -687,8 +695,8 @@ const Proposal = ({
                                           <div className="max-w-[80px] break-words sm:max-w-none sm:break-normal">
                                             {key}
                                           </div>
-                                          <div className="space-y-2 sm:flex sm:space-x-2">
-                                            <div className="flex items-center space-x-2 sm:mt-2 sm:ml-2">
+                                          <div className="space-y-2 sm:flex sm:space-y-0 sm:space-x-2">
+                                            <div className="flex items-center space-x-2 sm:ml-2">
                                               {parsedInstruction.accounts.named[
                                                 key
                                               ].isSigner ? (
@@ -700,38 +708,44 @@ const Proposal = ({
                                                 <WritableTag />
                                               ) : null}
                                             </div>
-                                            <div
-                                              className="-ml-1 inline-flex cursor-pointer items-center px-1 hover:bg-dark hover:text-white active:bg-darkGray3"
-                                              onClick={() => {
-                                                copy(
-                                                  parsedInstruction.accounts.named[
-                                                    key
-                                                  ].pubkey.toBase58()
-                                                )
-                                              }}
-                                            >
-                                              <span className="mr-2 hidden xl:block">
-                                                {parsedInstruction.accounts.named[
-                                                  key
-                                                ].pubkey.toBase58()}
-                                              </span>
-                                              <span className="mr-2 xl:hidden">
-                                                {parsedInstruction.accounts.named[
-                                                  key
-                                                ].pubkey
-                                                  .toBase58()
-                                                  .slice(0, 6) +
-                                                  '...' +
-                                                  parsedInstruction.accounts.named[
-                                                    key
-                                                  ].pubkey
-                                                    .toBase58()
-                                                    .slice(-6)}
-                                              </span>{' '}
-                                              <CopyIcon className="shrink-0" />
-                                            </div>
+                                            <CopyPubkey
+                                              pubkey={parsedInstruction.accounts.named[
+                                                key
+                                              ].pubkey.toBase58()}
+                                            />
                                           </div>
                                         </div>
+                                        {key === 'priceAccount' &&
+                                        parsedInstruction.accounts.named[
+                                          key
+                                        ].pubkey.toBase58() in
+                                          priceAccountKeyToSymbolMapping ? (
+                                          <ParsedAccountPubkeyRow
+                                            key="priceAccountPubkey"
+                                            mapping={
+                                              priceAccountKeyToSymbolMapping
+                                            }
+                                            title="symbol"
+                                            pubkey={parsedInstruction.accounts.named[
+                                              key
+                                            ].pubkey.toBase58()}
+                                          />
+                                        ) : key === 'productAccount' &&
+                                          parsedInstruction.accounts.named[
+                                            key
+                                          ].pubkey.toBase58() in
+                                            productAccountKeyToSymbolMapping ? (
+                                          <ParsedAccountPubkeyRow
+                                            key="productAccountPubkey"
+                                            mapping={
+                                              productAccountKeyToSymbolMapping
+                                            }
+                                            title="symbol"
+                                            pubkey={parsedInstruction.accounts.named[
+                                              key
+                                            ].pubkey.toBase58()}
+                                          />
+                                        ) : null}
                                       </>
                                     ))}
                                   </div>
@@ -790,26 +804,9 @@ const Proposal = ({
                                               {key.isWritable ? (
                                                 <WritableTag />
                                               ) : null}
-                                              <div
-                                                className="-ml-1 inline-flex cursor-pointer items-center px-1 hover:bg-dark hover:text-white active:bg-darkGray3"
-                                                onClick={() => {
-                                                  copy(key.pubkey.toBase58())
-                                                }}
-                                              >
-                                                <span className="mr-2 hidden xl:block">
-                                                  {key.pubkey.toBase58()}
-                                                </span>
-                                                <span className="mr-2 xl:hidden">
-                                                  {key.pubkey
-                                                    .toBase58()
-                                                    .slice(0, 6) +
-                                                    '...' +
-                                                    key.pubkey
-                                                      .toBase58()
-                                                      .slice(-6)}
-                                                </span>{' '}
-                                                <CopyIcon className="shrink-0" />
-                                              </div>
+                                              <CopyPubkey
+                                                pubkey={key.pubkey.toBase58()}
+                                              />
                                             </div>
                                           </div>
                                         </>
@@ -827,7 +824,7 @@ const Proposal = ({
               </div>
             ) : null}
 
-            {index !== proposalInstructions.length - 1 ? (
+            {index !== instructions.length - 1 ? (
               <hr className="border-gray-700" />
             ) : null}
           </>
@@ -844,13 +841,60 @@ const Proposal = ({
 const Proposals = () => {
   const router = useRouter()
   const [currentProposal, setCurrentProposal] = useState<TransactionAccount>()
+  const [currentProposalIndex, setCurrentProposalIndex] = useState<number>()
+  const [allProposalsVerifiedArr, setAllProposalsVerifiedArr] = useState<
+    boolean[]
+  >([])
   const [currentProposalPubkey, setCurrentProposalPubkey] = useState<string>()
+  const { cluster } = useContext(ClusterContext)
   const {
     priceFeedMultisigAccount,
     priceFeedMultisigProposals,
+    allProposalsIxsParsed,
     isLoading: isMultisigLoading,
   } = useMultisigContext()
 
+  useEffect(() => {
+    if (!isMultisigLoading) {
+      const res: boolean[] = []
+      allProposalsIxsParsed.map((ixs, idx) => {
+        const isAllIxsVerified =
+          ixs.length > 0 &&
+          ixs.every(
+            (ix) =>
+              ix instanceof PythMultisigInstruction ||
+              (ix instanceof WormholeMultisigInstruction &&
+                ix.name === 'postMessage' &&
+                ix.governanceAction instanceof ExecutePostedVaa &&
+                ix.governanceAction.instructions.every((remoteIx) => {
+                  const innerMultisigParser = MultisigParser.fromCluster(
+                    getRemoteCluster(cluster)
+                  )
+                  const parsedRemoteInstruction =
+                    innerMultisigParser.parseInstruction({
+                      programId: remoteIx.programId,
+                      data: remoteIx.data as Buffer,
+                      keys: remoteIx.keys as AccountMeta[],
+                    })
+                  return (
+                    parsedRemoteInstruction instanceof PythMultisigInstruction
+                  )
+                }) &&
+                ix.governanceAction.targetChainId === 'pythnet')
+          ) &&
+          Object.keys(priceFeedMultisigProposals[idx].status)[0] !== 'draft'
+
+        res.push(isAllIxsVerified)
+      })
+      setAllProposalsVerifiedArr(res)
+    }
+  }, [
+    allProposalsIxsParsed,
+    isMultisigLoading,
+    cluster,
+    priceFeedMultisigProposals,
+  ])
+
   const handleClickBackToPriceFeeds = () => {
     delete router.query.proposal
     router.push(
@@ -871,12 +915,23 @@ const Proposals = () => {
 
   useEffect(() => {
     if (currentProposalPubkey) {
-      const currentProposal = priceFeedMultisigProposals.find(
+      const currProposal = priceFeedMultisigProposals.find(
         (proposal) => proposal.publicKey.toBase58() === currentProposalPubkey
       )
-      setCurrentProposal(currentProposal)
+      const currProposalIndex = priceFeedMultisigProposals.findIndex(
+        (proposal) => proposal.publicKey.toBase58() === currentProposalPubkey
+      )
+      setCurrentProposal(currProposal)
+      setCurrentProposalIndex(
+        currProposalIndex === -1 ? undefined : currProposalIndex
+      )
     }
-  }, [currentProposalPubkey, priceFeedMultisigProposals])
+  }, [
+    currentProposalPubkey,
+    priceFeedMultisigProposals,
+    allProposalsIxsParsed,
+    cluster,
+  ])
 
   return (
     <div className="relative">
@@ -901,21 +956,29 @@ const Proposals = () => {
                   <Loadbar theme="light" />
                 </div>
               ) : priceFeedMultisigProposals.length > 0 ? (
-                <div className="flex flex-col">
-                  {priceFeedMultisigProposals.map((proposal, idx) => (
-                    <ProposalRow
-                      key={idx}
-                      proposal={proposal}
-                      setCurrentProposalPubkey={setCurrentProposalPubkey}
-                    />
-                  ))}
-                </div>
+                <>
+                  <div className="pb-4">
+                    <h4 className="h4">
+                      Total Proposals: {priceFeedMultisigProposals.length}
+                    </h4>
+                  </div>
+                  <div className="flex flex-col">
+                    {priceFeedMultisigProposals.map((proposal, idx) => (
+                      <ProposalRow
+                        key={idx}
+                        proposal={proposal}
+                        verified={allProposalsVerifiedArr[idx]}
+                        setCurrentProposalPubkey={setCurrentProposalPubkey}
+                      />
+                    ))}
+                  </div>
+                </>
               ) : (
                 "No proposals found. If you're a member of the price feed multisig, you can create a proposal."
               )}
             </div>
           </>
-        ) : (
+        ) : !isMultisigLoading && currentProposalIndex !== 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"
@@ -926,10 +989,16 @@ const Proposals = () => {
             <div className="relative mt-6">
               <Proposal
                 proposal={currentProposal}
+                instructions={allProposalsIxsParsed[currentProposalIndex]}
+                verified={allProposalsVerifiedArr[currentProposalIndex]}
                 multisig={priceFeedMultisigAccount}
               />
             </div>
           </>
+        ) : (
+          <div className="mt-3">
+            <Loadbar theme="light" />
+          </div>
         )}
       </div>
     </div>

+ 6 - 0
governance/xc_admin/packages/xc_admin_frontend/contexts/MultisigContext.tsx

@@ -3,6 +3,7 @@ import { useAnchorWallet } from '@solana/wallet-adapter-react'
 import SquadsMesh from '@sqds/mesh'
 import { MultisigAccount, TransactionAccount } from '@sqds/mesh/lib/types'
 import React, { createContext, useContext, useMemo } from 'react'
+import { MultisigInstruction } from 'xc_admin_common'
 import { useMultisig } from '../hooks/useMultisig'
 
 // TODO: fix any
@@ -14,6 +15,7 @@ interface MultisigContextProps {
   priceFeedMultisigAccount: MultisigAccount | undefined
   upgradeMultisigProposals: TransactionAccount[]
   priceFeedMultisigProposals: TransactionAccount[]
+  allProposalsIxsParsed: MultisigInstruction[][]
 }
 
 const MultisigContext = createContext<MultisigContextProps>({
@@ -21,6 +23,7 @@ const MultisigContext = createContext<MultisigContextProps>({
   priceFeedMultisigAccount: undefined,
   upgradeMultisigProposals: [],
   priceFeedMultisigProposals: [],
+  allProposalsIxsParsed: [],
   isLoading: true,
   error: null,
   squads: undefined,
@@ -44,6 +47,7 @@ export const MultisigContextProvider: React.FC<
     priceFeedMultisigAccount,
     upgradeMultisigProposals,
     priceFeedMultisigProposals,
+    allProposalsIxsParsed,
   } = useMultisig(anchorWallet as Wallet)
 
   const value = useMemo(
@@ -52,6 +56,7 @@ export const MultisigContextProvider: React.FC<
       priceFeedMultisigAccount,
       upgradeMultisigProposals,
       priceFeedMultisigProposals,
+      allProposalsIxsParsed,
       isLoading,
       error,
       squads,
@@ -64,6 +69,7 @@ export const MultisigContextProvider: React.FC<
       priceFeedMultisigAccount,
       upgradeMultisigProposals,
       priceFeedMultisigProposals,
+      allProposalsIxsParsed,
     ]
   )
 

+ 68 - 6
governance/xc_admin/packages/xc_admin_frontend/hooks/useMultisig.ts

@@ -1,9 +1,25 @@
 import { Wallet } from '@coral-xyz/anchor'
-import { Cluster, Connection, PublicKey, Transaction } from '@solana/web3.js'
+import {
+  AccountMeta,
+  Cluster,
+  Connection,
+  PublicKey,
+  Transaction,
+} from '@solana/web3.js'
 import SquadsMesh from '@sqds/mesh'
 import { MultisigAccount, TransactionAccount } from '@sqds/mesh/lib/types'
 import { useContext, useEffect, useRef, useState } from 'react'
-import { getMultisigCluster, getProposals } from 'xc_admin_common'
+import {
+  getManyProposalsInstructions,
+  getMultisigCluster,
+  getProposals,
+  isRemoteCluster,
+  MultisigInstruction,
+  MultisigParser,
+  PythMultisigInstruction,
+  UnrecognizedProgram,
+  WormholeMultisigInstruction,
+} from 'xc_admin_common'
 import { ClusterContext } from '../contexts/ClusterContext'
 import { pythClusterApiUrls } from '../utils/pythClusterApiUrl'
 
@@ -29,6 +45,7 @@ interface MultisigHookData {
   priceFeedMultisigAccount: MultisigAccount | undefined
   upgradeMultisigProposals: TransactionAccount[]
   priceFeedMultisigProposals: TransactionAccount[]
+  allProposalsIxsParsed: MultisigInstruction[][]
 }
 
 const getSortedProposals = async (
@@ -54,6 +71,9 @@ export const useMultisig = (wallet: Wallet): MultisigHookData => {
   const [priceFeedMultisigProposals, setpriceFeedMultisigProposals] = useState<
     TransactionAccount[]
   >([])
+  const [allProposalsIxsParsed, setAllProposalsIxsParsed] = useState<
+    MultisigInstruction[][]
+  >([])
   const [squads, setSquads] = useState<SquadsMesh>()
   const [urlsIndex, setUrlsIndex] = useState(0)
 
@@ -124,14 +144,55 @@ export const useMultisig = (wallet: Wallet): MultisigHookData => {
         try {
           if (cancelled) return
           // DELETE THIS TRY CATCH ONCE THIS MULTISIG EXISTS EVERYWHERE
-          setpriceFeedMultisigProposals(
-            await getSortedProposals(
-              squads,
-              PRICE_FEED_MULTISIG[getMultisigCluster(cluster)]
+          const sortedPriceFeedMultisigProposals = await getSortedProposals(
+            squads,
+            PRICE_FEED_MULTISIG[getMultisigCluster(cluster)]
+          )
+          const allProposalsIxs = await getManyProposalsInstructions(
+            squads,
+            sortedPriceFeedMultisigProposals
+          )
+          const multisigParser = MultisigParser.fromCluster(
+            getMultisigCluster(cluster)
+          )
+          const parsedAllProposalsIxs = allProposalsIxs.map((ixs) =>
+            ixs.map((ix) =>
+              multisigParser.parseInstruction({
+                programId: ix.programId,
+                data: ix.data as Buffer,
+                keys: ix.keys as AccountMeta[],
+              })
             )
           )
+          const proposalsRes: TransactionAccount[] = []
+          const instructionsRes: MultisigInstruction[][] = []
+          // filter proposals for respective devnet/pythtest and mainnet-beta/pythnet clusters
+          parsedAllProposalsIxs.map((ixs, idx) => {
+            // pythtest/pythnet proposals
+            if (
+              isRemoteCluster(cluster) &&
+              ixs.length > 0 &&
+              ixs.some((ix) => ix instanceof WormholeMultisigInstruction)
+            ) {
+              proposalsRes.push(sortedPriceFeedMultisigProposals[idx])
+              instructionsRes.push(ixs)
+            }
+            // devnet/testnet/mainnet-beta proposals
+            if (
+              !isRemoteCluster(cluster) &&
+              (ixs.length === 0 ||
+                ixs.some((ix) => ix instanceof PythMultisigInstruction) ||
+                ixs.some((ix) => ix instanceof UnrecognizedProgram))
+            ) {
+              proposalsRes.push(sortedPriceFeedMultisigProposals[idx])
+              instructionsRes.push(ixs)
+            }
+          })
+          setAllProposalsIxsParsed(instructionsRes)
+          setpriceFeedMultisigProposals(proposalsRes)
         } catch (e) {
           console.error(e)
+          setAllProposalsIxsParsed([])
           setpriceFeedMultisigProposals([])
         }
 
@@ -167,5 +228,6 @@ export const useMultisig = (wallet: Wallet): MultisigHookData => {
     priceFeedMultisigAccount,
     upgradeMultisigProposals,
     priceFeedMultisigProposals,
+    allProposalsIxsParsed,
   }
 }

+ 3 - 0
governance/xc_admin/packages/xc_admin_frontend/images/icons/verified.inline.svg

@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M18.5416 10C18.5416 8.80833 17.8083 7.775 16.7166 7.21667C17.1 6.05833 16.8833 4.8 16.0416 3.95833C15.2 3.11667 13.9416 2.9 12.7833 3.28333C12.2333 2.19167 11.1916 1.45833 9.99998 1.45833C8.80831 1.45833 7.77498 2.19167 7.22498 3.28333C6.05831 2.9 4.79998 3.11667 3.95831 3.95833C3.11665 4.8 2.90831 6.05833 3.29165 7.21667C2.19998 7.775 1.45831 8.80833 1.45831 10C1.45831 11.1917 2.19998 12.225 3.29165 12.7833C2.90831 13.9417 3.11665 15.2 3.95831 16.0417C4.79998 16.8833 6.05831 17.0917 7.21665 16.7167C7.77498 17.8083 8.80831 18.5417 9.99998 18.5417C11.1916 18.5417 12.2333 17.8083 12.7833 16.7167C13.9416 17.0917 15.2 16.8833 16.0416 16.0417C16.8833 15.2 17.1 13.9417 16.7166 12.7833C17.8083 12.225 18.5416 11.1917 18.5416 10ZM8.78331 13.5L5.66665 10.3833L6.84165 9.2L8.72498 11.0833L12.725 6.725L13.95 7.85833L8.78331 13.5Z" fill="#E6DAFE"/>
+</svg>

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

@@ -13,6 +13,7 @@
     "@headlessui/react": "^1.7.7",
     "@pythnetwork/client": "^2.15.0",
     "@solana/spl-token": "^0.3.7",
+    "@radix-ui/react-tooltip": "^1.0.3",
     "@solana/wallet-adapter-base": "^0.9.20",
     "@solana/wallet-adapter-react": "^0.15.28",
     "@solana/wallet-adapter-react-ui": "^0.9.27",

+ 529 - 0
package-lock.json

@@ -1416,6 +1416,7 @@
         "@coral-xyz/anchor": "^0.26.0",
         "@headlessui/react": "^1.7.7",
         "@pythnetwork/client": "^2.15.0",
+        "@radix-ui/react-tooltip": "^1.0.3",
         "@solana/spl-token": "^0.3.7",
         "@solana/wallet-adapter-base": "^0.9.20",
         "@solana/wallet-adapter-react": "^0.15.28",
@@ -6348,6 +6349,32 @@
         "@ethersproject/strings": "^5.7.0"
       }
     },
+    "node_modules/@floating-ui/core": {
+      "version": "0.7.3",
+      "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-0.7.3.tgz",
+      "integrity": "sha512-buc8BXHmG9l82+OQXOFU3Kr2XQx9ys01U/Q9HMIrZ300iLc8HLMgh7dcCqgYzAzf4BkoQvDcXf5Y+CuEZ5JBYg=="
+    },
+    "node_modules/@floating-ui/dom": {
+      "version": "0.5.4",
+      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-0.5.4.tgz",
+      "integrity": "sha512-419BMceRLq0RrmTSDxn8hf9R3VCJv2K9PUfugh5JyEFmdjzDo+e8U5EdR8nzKq8Yj1htzLm3b6eQEEam3/rrtg==",
+      "dependencies": {
+        "@floating-ui/core": "^0.7.3"
+      }
+    },
+    "node_modules/@floating-ui/react-dom": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-0.7.2.tgz",
+      "integrity": "sha512-1T0sJcpHgX/u4I1OzIEhlcrvkUN8ln39nz7fMoE/2HDHrPiMFoOGR7++GYyfUmIQHkkrTinaeQsO3XWubjSvGg==",
+      "dependencies": {
+        "@floating-ui/dom": "^0.5.3",
+        "use-isomorphic-layout-effect": "^1.1.1"
+      },
+      "peerDependencies": {
+        "react": ">=16.8.0",
+        "react-dom": ">=16.8.0"
+      }
+    },
     "node_modules/@fractalwagmi/popup-connection": {
       "version": "1.0.21",
       "resolved": "https://registry.npmjs.org/@fractalwagmi/popup-connection/-/popup-connection-1.0.21.tgz",
@@ -10965,6 +10992,267 @@
       "resolved": "governance/xc_governance_sdk_js",
       "link": true
     },
+    "node_modules/@radix-ui/primitive": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz",
+      "integrity": "sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10"
+      }
+    },
+    "node_modules/@radix-ui/react-arrow": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz",
+      "integrity": "sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/react-primitive": "1.0.1"
+      },
+      "peerDependencies": {
+        "react": "^16.8 || ^17.0 || ^18.0",
+        "react-dom": "^16.8 || ^17.0 || ^18.0"
+      }
+    },
+    "node_modules/@radix-ui/react-compose-refs": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz",
+      "integrity": "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10"
+      },
+      "peerDependencies": {
+        "react": "^16.8 || ^17.0 || ^18.0"
+      }
+    },
+    "node_modules/@radix-ui/react-context": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.0.tgz",
+      "integrity": "sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10"
+      },
+      "peerDependencies": {
+        "react": "^16.8 || ^17.0 || ^18.0"
+      }
+    },
+    "node_modules/@radix-ui/react-dismissable-layer": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.2.tgz",
+      "integrity": "sha512-WjJzMrTWROozDqLB0uRWYvj4UuXsM/2L19EmQ3Au+IJWqwvwq9Bwd+P8ivo0Deg9JDPArR1I6MbWNi1CmXsskg==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/primitive": "1.0.0",
+        "@radix-ui/react-compose-refs": "1.0.0",
+        "@radix-ui/react-primitive": "1.0.1",
+        "@radix-ui/react-use-callback-ref": "1.0.0",
+        "@radix-ui/react-use-escape-keydown": "1.0.2"
+      },
+      "peerDependencies": {
+        "react": "^16.8 || ^17.0 || ^18.0",
+        "react-dom": "^16.8 || ^17.0 || ^18.0"
+      }
+    },
+    "node_modules/@radix-ui/react-id": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.0.tgz",
+      "integrity": "sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/react-use-layout-effect": "1.0.0"
+      },
+      "peerDependencies": {
+        "react": "^16.8 || ^17.0 || ^18.0"
+      }
+    },
+    "node_modules/@radix-ui/react-popper": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.0.tgz",
+      "integrity": "sha512-07U7jpI0dZcLRAxT7L9qs6HecSoPhDSJybF7mEGHJDBDv+ZoGCvIlva0s+WxMXwJEav+ckX3hAlXBtnHmuvlCQ==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10",
+        "@floating-ui/react-dom": "0.7.2",
+        "@radix-ui/react-arrow": "1.0.1",
+        "@radix-ui/react-compose-refs": "1.0.0",
+        "@radix-ui/react-context": "1.0.0",
+        "@radix-ui/react-primitive": "1.0.1",
+        "@radix-ui/react-use-callback-ref": "1.0.0",
+        "@radix-ui/react-use-layout-effect": "1.0.0",
+        "@radix-ui/react-use-rect": "1.0.0",
+        "@radix-ui/react-use-size": "1.0.0",
+        "@radix-ui/rect": "1.0.0"
+      },
+      "peerDependencies": {
+        "react": "^16.8 || ^17.0 || ^18.0",
+        "react-dom": "^16.8 || ^17.0 || ^18.0"
+      }
+    },
+    "node_modules/@radix-ui/react-portal": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.1.tgz",
+      "integrity": "sha512-NY2vUWI5WENgAT1nfC6JS7RU5xRYBfjZVLq0HmgEN1Ezy3rk/UruMV4+Rd0F40PEaFC5SrLS1ixYvcYIQrb4Ig==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/react-primitive": "1.0.1"
+      },
+      "peerDependencies": {
+        "react": "^16.8 || ^17.0 || ^18.0",
+        "react-dom": "^16.8 || ^17.0 || ^18.0"
+      }
+    },
+    "node_modules/@radix-ui/react-presence": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.0.tgz",
+      "integrity": "sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/react-compose-refs": "1.0.0",
+        "@radix-ui/react-use-layout-effect": "1.0.0"
+      },
+      "peerDependencies": {
+        "react": "^16.8 || ^17.0 || ^18.0",
+        "react-dom": "^16.8 || ^17.0 || ^18.0"
+      }
+    },
+    "node_modules/@radix-ui/react-primitive": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.1.tgz",
+      "integrity": "sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/react-slot": "1.0.1"
+      },
+      "peerDependencies": {
+        "react": "^16.8 || ^17.0 || ^18.0",
+        "react-dom": "^16.8 || ^17.0 || ^18.0"
+      }
+    },
+    "node_modules/@radix-ui/react-slot": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.1.tgz",
+      "integrity": "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/react-compose-refs": "1.0.0"
+      },
+      "peerDependencies": {
+        "react": "^16.8 || ^17.0 || ^18.0"
+      }
+    },
+    "node_modules/@radix-ui/react-tooltip": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.3.tgz",
+      "integrity": "sha512-cmc9qV4KpgqdXVTn1K8KN8MnuSXvw+E719pKwyvpCGrQ+0AA2qTjcIL3uxCj4jc4k3sDR36RF7R3H7N5hPybBQ==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/primitive": "1.0.0",
+        "@radix-ui/react-compose-refs": "1.0.0",
+        "@radix-ui/react-context": "1.0.0",
+        "@radix-ui/react-dismissable-layer": "1.0.2",
+        "@radix-ui/react-id": "1.0.0",
+        "@radix-ui/react-popper": "1.1.0",
+        "@radix-ui/react-portal": "1.0.1",
+        "@radix-ui/react-presence": "1.0.0",
+        "@radix-ui/react-primitive": "1.0.1",
+        "@radix-ui/react-slot": "1.0.1",
+        "@radix-ui/react-use-controllable-state": "1.0.0",
+        "@radix-ui/react-visually-hidden": "1.0.1"
+      },
+      "peerDependencies": {
+        "react": "^16.8 || ^17.0 || ^18.0",
+        "react-dom": "^16.8 || ^17.0 || ^18.0"
+      }
+    },
+    "node_modules/@radix-ui/react-use-callback-ref": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz",
+      "integrity": "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10"
+      },
+      "peerDependencies": {
+        "react": "^16.8 || ^17.0 || ^18.0"
+      }
+    },
+    "node_modules/@radix-ui/react-use-controllable-state": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.0.tgz",
+      "integrity": "sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/react-use-callback-ref": "1.0.0"
+      },
+      "peerDependencies": {
+        "react": "^16.8 || ^17.0 || ^18.0"
+      }
+    },
+    "node_modules/@radix-ui/react-use-escape-keydown": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.2.tgz",
+      "integrity": "sha512-DXGim3x74WgUv+iMNCF+cAo8xUHHeqvjx8zs7trKf+FkQKPQXLk2sX7Gx1ysH7Q76xCpZuxIJE7HLPxRE+Q+GA==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/react-use-callback-ref": "1.0.0"
+      },
+      "peerDependencies": {
+        "react": "^16.8 || ^17.0 || ^18.0"
+      }
+    },
+    "node_modules/@radix-ui/react-use-layout-effect": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.0.tgz",
+      "integrity": "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10"
+      },
+      "peerDependencies": {
+        "react": "^16.8 || ^17.0 || ^18.0"
+      }
+    },
+    "node_modules/@radix-ui/react-use-rect": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.0.tgz",
+      "integrity": "sha512-TB7pID8NRMEHxb/qQJpvSt3hQU4sqNPM1VCTjTRjEOa7cEop/QMuq8S6fb/5Tsz64kqSvB9WnwsDHtjnrM9qew==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/rect": "1.0.0"
+      },
+      "peerDependencies": {
+        "react": "^16.8 || ^17.0 || ^18.0"
+      }
+    },
+    "node_modules/@radix-ui/react-use-size": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.0.0.tgz",
+      "integrity": "sha512-imZ3aYcoYCKhhgNpkNDh/aTiU05qw9hX+HHI1QDBTyIlcFjgeFlKKySNGMwTp7nYFLQg/j0VA2FmCY4WPDDHMg==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/react-use-layout-effect": "1.0.0"
+      },
+      "peerDependencies": {
+        "react": "^16.8 || ^17.0 || ^18.0"
+      }
+    },
+    "node_modules/@radix-ui/react-visually-hidden": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.1.tgz",
+      "integrity": "sha512-K1hJcCMfWfiYUibRqf3V8r5Drpyf7rh44jnrwAbdvI5iCCijilBBeyQv9SKidYNZIopMdCyR9FnIjkHxHN0FcQ==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/react-primitive": "1.0.1"
+      },
+      "peerDependencies": {
+        "react": "^16.8 || ^17.0 || ^18.0",
+        "react-dom": "^16.8 || ^17.0 || ^18.0"
+      }
+    },
+    "node_modules/@radix-ui/rect": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.0.0.tgz",
+      "integrity": "sha512-d0O68AYy/9oeEy1DdC07bz1/ZXX+DqCskRd3i4JzLSTXwefzaepQrKjXC7aNM8lTHjFLDO0pDgaEiQ7jEk+HVg==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10"
+      }
+    },
     "node_modules/@react-native-async-storage/async-storage": {
       "version": "1.17.11",
       "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.17.11.tgz",
@@ -44382,6 +44670,19 @@
         "react": ">=16.8.0"
       }
     },
+    "node_modules/use-isomorphic-layout-effect": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz",
+      "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==",
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/use-sync-external-store": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
@@ -50357,6 +50658,28 @@
         "@ethersproject/strings": "^5.7.0"
       }
     },
+    "@floating-ui/core": {
+      "version": "0.7.3",
+      "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-0.7.3.tgz",
+      "integrity": "sha512-buc8BXHmG9l82+OQXOFU3Kr2XQx9ys01U/Q9HMIrZ300iLc8HLMgh7dcCqgYzAzf4BkoQvDcXf5Y+CuEZ5JBYg=="
+    },
+    "@floating-ui/dom": {
+      "version": "0.5.4",
+      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-0.5.4.tgz",
+      "integrity": "sha512-419BMceRLq0RrmTSDxn8hf9R3VCJv2K9PUfugh5JyEFmdjzDo+e8U5EdR8nzKq8Yj1htzLm3b6eQEEam3/rrtg==",
+      "requires": {
+        "@floating-ui/core": "^0.7.3"
+      }
+    },
+    "@floating-ui/react-dom": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-0.7.2.tgz",
+      "integrity": "sha512-1T0sJcpHgX/u4I1OzIEhlcrvkUN8ln39nz7fMoE/2HDHrPiMFoOGR7++GYyfUmIQHkkrTinaeQsO3XWubjSvGg==",
+      "requires": {
+        "@floating-ui/dom": "^0.5.3",
+        "use-isomorphic-layout-effect": "^1.1.1"
+      }
+    },
     "@fractalwagmi/popup-connection": {
       "version": "1.0.21",
       "resolved": "https://registry.npmjs.org/@fractalwagmi/popup-connection/-/popup-connection-1.0.21.tgz",
@@ -56136,6 +56459,205 @@
         }
       }
     },
+    "@radix-ui/primitive": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz",
+      "integrity": "sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==",
+      "requires": {
+        "@babel/runtime": "^7.13.10"
+      }
+    },
+    "@radix-ui/react-arrow": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz",
+      "integrity": "sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==",
+      "requires": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/react-primitive": "1.0.1"
+      }
+    },
+    "@radix-ui/react-compose-refs": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz",
+      "integrity": "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==",
+      "requires": {
+        "@babel/runtime": "^7.13.10"
+      }
+    },
+    "@radix-ui/react-context": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.0.tgz",
+      "integrity": "sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==",
+      "requires": {
+        "@babel/runtime": "^7.13.10"
+      }
+    },
+    "@radix-ui/react-dismissable-layer": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.2.tgz",
+      "integrity": "sha512-WjJzMrTWROozDqLB0uRWYvj4UuXsM/2L19EmQ3Au+IJWqwvwq9Bwd+P8ivo0Deg9JDPArR1I6MbWNi1CmXsskg==",
+      "requires": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/primitive": "1.0.0",
+        "@radix-ui/react-compose-refs": "1.0.0",
+        "@radix-ui/react-primitive": "1.0.1",
+        "@radix-ui/react-use-callback-ref": "1.0.0",
+        "@radix-ui/react-use-escape-keydown": "1.0.2"
+      }
+    },
+    "@radix-ui/react-id": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.0.tgz",
+      "integrity": "sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==",
+      "requires": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/react-use-layout-effect": "1.0.0"
+      }
+    },
+    "@radix-ui/react-popper": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.0.tgz",
+      "integrity": "sha512-07U7jpI0dZcLRAxT7L9qs6HecSoPhDSJybF7mEGHJDBDv+ZoGCvIlva0s+WxMXwJEav+ckX3hAlXBtnHmuvlCQ==",
+      "requires": {
+        "@babel/runtime": "^7.13.10",
+        "@floating-ui/react-dom": "0.7.2",
+        "@radix-ui/react-arrow": "1.0.1",
+        "@radix-ui/react-compose-refs": "1.0.0",
+        "@radix-ui/react-context": "1.0.0",
+        "@radix-ui/react-primitive": "1.0.1",
+        "@radix-ui/react-use-callback-ref": "1.0.0",
+        "@radix-ui/react-use-layout-effect": "1.0.0",
+        "@radix-ui/react-use-rect": "1.0.0",
+        "@radix-ui/react-use-size": "1.0.0",
+        "@radix-ui/rect": "1.0.0"
+      }
+    },
+    "@radix-ui/react-portal": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.1.tgz",
+      "integrity": "sha512-NY2vUWI5WENgAT1nfC6JS7RU5xRYBfjZVLq0HmgEN1Ezy3rk/UruMV4+Rd0F40PEaFC5SrLS1ixYvcYIQrb4Ig==",
+      "requires": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/react-primitive": "1.0.1"
+      }
+    },
+    "@radix-ui/react-presence": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.0.tgz",
+      "integrity": "sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==",
+      "requires": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/react-compose-refs": "1.0.0",
+        "@radix-ui/react-use-layout-effect": "1.0.0"
+      }
+    },
+    "@radix-ui/react-primitive": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.1.tgz",
+      "integrity": "sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA==",
+      "requires": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/react-slot": "1.0.1"
+      }
+    },
+    "@radix-ui/react-slot": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.1.tgz",
+      "integrity": "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==",
+      "requires": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/react-compose-refs": "1.0.0"
+      }
+    },
+    "@radix-ui/react-tooltip": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.3.tgz",
+      "integrity": "sha512-cmc9qV4KpgqdXVTn1K8KN8MnuSXvw+E719pKwyvpCGrQ+0AA2qTjcIL3uxCj4jc4k3sDR36RF7R3H7N5hPybBQ==",
+      "requires": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/primitive": "1.0.0",
+        "@radix-ui/react-compose-refs": "1.0.0",
+        "@radix-ui/react-context": "1.0.0",
+        "@radix-ui/react-dismissable-layer": "1.0.2",
+        "@radix-ui/react-id": "1.0.0",
+        "@radix-ui/react-popper": "1.1.0",
+        "@radix-ui/react-portal": "1.0.1",
+        "@radix-ui/react-presence": "1.0.0",
+        "@radix-ui/react-primitive": "1.0.1",
+        "@radix-ui/react-slot": "1.0.1",
+        "@radix-ui/react-use-controllable-state": "1.0.0",
+        "@radix-ui/react-visually-hidden": "1.0.1"
+      }
+    },
+    "@radix-ui/react-use-callback-ref": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz",
+      "integrity": "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==",
+      "requires": {
+        "@babel/runtime": "^7.13.10"
+      }
+    },
+    "@radix-ui/react-use-controllable-state": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.0.tgz",
+      "integrity": "sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg==",
+      "requires": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/react-use-callback-ref": "1.0.0"
+      }
+    },
+    "@radix-ui/react-use-escape-keydown": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.2.tgz",
+      "integrity": "sha512-DXGim3x74WgUv+iMNCF+cAo8xUHHeqvjx8zs7trKf+FkQKPQXLk2sX7Gx1ysH7Q76xCpZuxIJE7HLPxRE+Q+GA==",
+      "requires": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/react-use-callback-ref": "1.0.0"
+      }
+    },
+    "@radix-ui/react-use-layout-effect": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.0.tgz",
+      "integrity": "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==",
+      "requires": {
+        "@babel/runtime": "^7.13.10"
+      }
+    },
+    "@radix-ui/react-use-rect": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.0.tgz",
+      "integrity": "sha512-TB7pID8NRMEHxb/qQJpvSt3hQU4sqNPM1VCTjTRjEOa7cEop/QMuq8S6fb/5Tsz64kqSvB9WnwsDHtjnrM9qew==",
+      "requires": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/rect": "1.0.0"
+      }
+    },
+    "@radix-ui/react-use-size": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.0.0.tgz",
+      "integrity": "sha512-imZ3aYcoYCKhhgNpkNDh/aTiU05qw9hX+HHI1QDBTyIlcFjgeFlKKySNGMwTp7nYFLQg/j0VA2FmCY4WPDDHMg==",
+      "requires": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/react-use-layout-effect": "1.0.0"
+      }
+    },
+    "@radix-ui/react-visually-hidden": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.1.tgz",
+      "integrity": "sha512-K1hJcCMfWfiYUibRqf3V8r5Drpyf7rh44jnrwAbdvI5iCCijilBBeyQv9SKidYNZIopMdCyR9FnIjkHxHN0FcQ==",
+      "requires": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/react-primitive": "1.0.1"
+      }
+    },
+    "@radix-ui/rect": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.0.0.tgz",
+      "integrity": "sha512-d0O68AYy/9oeEy1DdC07bz1/ZXX+DqCskRd3i4JzLSTXwefzaepQrKjXC7aNM8lTHjFLDO0pDgaEiQ7jEk+HVg==",
+      "requires": {
+        "@babel/runtime": "^7.13.10"
+      }
+    },
     "@react-native-async-storage/async-storage": {
       "version": "1.17.11",
       "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.17.11.tgz",
@@ -83790,6 +84312,12 @@
       "integrity": "sha512-FhtlbDtDXILJV7Lix5OZj5yX/fW1tzq+VrvK1fnT2bUrPOGruU9Rw8NCEn+UI9wopfERBEZAOQ8lfeCJPllgnw==",
       "requires": {}
     },
+    "use-isomorphic-layout-effect": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz",
+      "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==",
+      "requires": {}
+    },
     "use-sync-external-store": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
@@ -85167,6 +85695,7 @@
         "@headlessui/react": "^1.7.7",
         "@pythnetwork/client": "^2.15.0",
         "@solana/spl-token": "*",
+        "@radix-ui/react-tooltip": "*",
         "@solana/wallet-adapter-base": "^0.9.20",
         "@solana/wallet-adapter-react": "^0.15.28",
         "@solana/wallet-adapter-react-ui": "^0.9.27",