Xc admin frontend refactor (#964)

* Remove fetching proposal internals on page load

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

* Refactor squads creation based on wallet

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

* Disable propose button if no action and show hint

* Expose refresh functionality on multisig proposals

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

* Extract WormholeInstructionView as a separate component

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

* Add support for parsing governance instructions + minor refactors

* Add ability to show / approve upgrade proposals

* fix buttons overflow

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

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

---------

Co-authored-by: Daniel Chew <cctdaniel@outlook.com>
This commit is contained in:
Mohammad Amin Khashkhashi Moghaddam 2023-07-20 12:18:05 +02:00 committed by GitHub
parent 0e5d7d0470
commit f595d61ccd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 943 additions and 873 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,4 @@
import { Wallet } from '@coral-xyz/anchor'
import NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet'
import { Tab } from '@headlessui/react'
import { useAnchorWallet } from '@solana/wallet-adapter-react'
import { Keypair } from '@solana/web3.js'
import * as fs from 'fs'
import type { GetServerSideProps, NextPage } from 'next'
import { useRouter } from 'next/router'
@ -88,8 +84,6 @@ const Home: NextPage<{
}) => {
const [currentTabIndex, setCurrentTabIndex] = useState(0)
const tabInfoArray = Object.values(TAB_INFO)
const anchorWallet = useAnchorWallet()
const wallet = anchorWallet as Wallet
const router = useRouter()
@ -123,8 +117,11 @@ const Home: NextPage<{
return (
<Layout>
<PythContextProvider>
<MultisigContextProvider wallet={wallet}>
<PythContextProvider
publisherKeyToNameMapping={publisherKeyToNameMapping}
multisigSignerKeyToNameMapping={multisigSignerKeyToNameMapping}
>
<MultisigContextProvider>
<div className="container relative pt-16 md:pt-20">
<div className="py-8 md:py-16">
<Tab.Group
@ -150,20 +147,17 @@ const Home: NextPage<{
</div>
</div>
{tabInfoArray[currentTabIndex].queryString ===
TAB_INFO.General.queryString ? (
TAB_INFO.General.queryString && (
<General proposerServerUrl={proposerServerUrl} />
) : tabInfoArray[currentTabIndex].queryString ===
TAB_INFO.UpdatePermissions.queryString ? (
<UpdatePermissions />
) : tabInfoArray[currentTabIndex].queryString ===
TAB_INFO.Proposals.queryString ? (
)}
{tabInfoArray[currentTabIndex].queryString ===
TAB_INFO.UpdatePermissions.queryString && <UpdatePermissions />}
{tabInfoArray[currentTabIndex].queryString ===
TAB_INFO.Proposals.queryString && (
<StatusFilterProvider>
<Proposals
publisherKeyToNameMapping={publisherKeyToNameMapping}
multisigSignerKeyToNameMapping={multisigSignerKeyToNameMapping}
/>
<Proposals />
</StatusFilterProvider>
) : null}
)}
</MultisigContextProvider>
</PythContextProvider>
</Layout>

View File

@ -278,7 +278,7 @@
}
.dialogTitle {
@apply mb-8 text-center font-body text-[32px] leading-[1.1] lg:mb-11 lg:text-[44px] px-10;
@apply mb-8 px-10 text-center font-body text-[32px] leading-[1.1] lg:mb-11 lg:text-[44px];
}
.action-btn {