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> <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" <button
onClick={handleSendProposalButtonClick} className="action-btn text-base"
> onClick={handleSendProposalButtonClick}
{isSendProposalButtonLoading ? <Spinner /> : 'Send Proposal'} disabled={isSendProposalButtonLoading || !proposeSquads}
</button> >
) : null} {isSendProposalButtonLoading ? <Spinner /> : 'Send Proposal'}
</button>
{!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 SquadsMesh from '@sqds/mesh'
import { MultisigAccount, TransactionAccount } from '@sqds/mesh/lib/types' import { MultisigAccount, TransactionAccount } from '@sqds/mesh/lib/types'
import React, { createContext, useContext, useMemo } from 'react' import React, { createContext, useContext, useMemo } from 'react'
import { MultisigInstruction } from 'xc_admin_common' import { MultisigInstruction } from 'xc_admin_common'
import { useMultisig } from '../hooks/useMultisig' import { useMultisig, MultisigHookData } from '../hooks/useMultisig'
// TODO: fix any const MultisigContext = createContext<MultisigHookData>({
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>({
upgradeMultisigAccount: undefined, upgradeMultisigAccount: undefined,
priceFeedMultisigAccount: undefined, priceFeedMultisigAccount: undefined,
upgradeMultisigProposals: [], upgradeMultisigProposals: [],
@ -29,6 +14,8 @@ const MultisigContext = createContext<MultisigContextProps>({
error: null, error: null,
proposeSquads: undefined, proposeSquads: undefined,
voteSquads: undefined, voteSquads: undefined,
refreshData: undefined,
connection: undefined,
setpriceFeedMultisigProposals: () => {}, setpriceFeedMultisigProposals: () => {},
}) })
@ -36,12 +23,11 @@ export const useMultisigContext = () => useContext(MultisigContext)
interface MultisigContextProviderProps { interface MultisigContextProviderProps {
children?: React.ReactNode children?: React.ReactNode
wallet: Wallet
} }
export const MultisigContextProvider: React.FC< export const MultisigContextProvider: React.FC<
MultisigContextProviderProps MultisigContextProviderProps
> = ({ children, wallet }) => { > = ({ children }) => {
const { const {
isLoading, isLoading,
error, error,
@ -53,7 +39,9 @@ export const MultisigContextProvider: React.FC<
priceFeedMultisigProposals, priceFeedMultisigProposals,
allProposalsIxsParsed, allProposalsIxsParsed,
setpriceFeedMultisigProposals, setpriceFeedMultisigProposals,
} = useMultisig(wallet) refreshData,
connection,
} = useMultisig()
const value = useMemo( const value = useMemo(
() => ({ () => ({
@ -67,6 +55,8 @@ export const MultisigContextProvider: React.FC<
error, error,
proposeSquads, proposeSquads,
voteSquads, voteSquads,
refreshData,
connection,
}), }),
[ [
proposeSquads, proposeSquads,
@ -79,6 +69,8 @@ export const MultisigContextProvider: React.FC<
priceFeedMultisigProposals, priceFeedMultisigProposals,
allProposalsIxsParsed, allProposalsIxsParsed,
setpriceFeedMultisigProposals, 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 usePyth from '../hooks/usePyth'
import { RawConfig } from '../hooks/usePyth' import { RawConfig } from '../hooks/usePyth'
// TODO: fix any // TODO: fix any
type AccountKeyToSymbol = { [key: string]: string }
interface PythContextProps { interface PythContextProps {
rawConfig: RawConfig rawConfig: RawConfig
dataIsLoading: boolean dataIsLoading: boolean
error: any error: any
connection: any connection: any
priceAccountKeyToSymbolMapping: AccountKeyToSymbol
productAccountKeyToSymbolMapping: AccountKeyToSymbol
publisherKeyToNameMapping: Record<string, Record<string, string>>
multisigSignerKeyToNameMapping: Record<string, string>
} }
const PythContext = createContext<PythContextProps>({ const PythContext = createContext<PythContextProps>({
@ -15,20 +26,47 @@ const PythContext = createContext<PythContextProps>({
dataIsLoading: true, dataIsLoading: true,
error: null, error: null,
connection: null, connection: null,
priceAccountKeyToSymbolMapping: {},
productAccountKeyToSymbolMapping: {},
publisherKeyToNameMapping: {},
multisigSignerKeyToNameMapping: {},
}) })
export const usePythContext = () => useContext(PythContext) export const usePythContext = () => useContext(PythContext)
interface PythContextProviderProps { interface PythContextProviderProps {
children?: React.ReactNode children?: React.ReactNode
symbols?: string[] publisherKeyToNameMapping: Record<string, Record<string, string>>
raw?: boolean multisigSignerKeyToNameMapping: Record<string, string>
} }
export const PythContextProvider: React.FC<PythContextProviderProps> = ({ export const PythContextProvider: React.FC<PythContextProviderProps> = ({
children, children,
publisherKeyToNameMapping,
multisigSignerKeyToNameMapping,
}) => { }) => {
const { isLoading, error, connection, rawConfig } = usePyth() 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( const value = useMemo(
() => ({ () => ({
@ -36,8 +74,19 @@ export const PythContextProvider: React.FC<PythContextProviderProps> = ({
dataIsLoading: isLoading, dataIsLoading: isLoading,
error, error,
connection, connection,
priceAccountKeyToSymbolMapping,
productAccountKeyToSymbolMapping,
publisherKeyToNameMapping,
multisigSignerKeyToNameMapping,
}), }),
[rawConfig, isLoading, error, connection] [
rawConfig,
isLoading,
error,
connection,
publisherKeyToNameMapping,
multisigSignerKeyToNameMapping,
]
) )
return <PythContext.Provider value={value}>{children}</PythContext.Provider> 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 NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet'
import { getPythProgramKeyForCluster } from '@pythnetwork/client'
import { useAnchorWallet } from '@solana/wallet-adapter-react' import { useAnchorWallet } from '@solana/wallet-adapter-react'
import { import { Connection, Keypair, PublicKey } from '@solana/web3.js'
AccountMeta,
Cluster,
Connection,
Keypair,
PublicKey,
} from '@solana/web3.js'
import SquadsMesh from '@sqds/mesh' import SquadsMesh from '@sqds/mesh'
import { MultisigAccount, TransactionAccount } from '@sqds/mesh/lib/types' import { MultisigAccount, TransactionAccount } from '@sqds/mesh/lib/types'
import { useContext, useEffect, useState } from 'react' import { useCallback, useContext, useEffect, useMemo, useState } from 'react'
import { import {
ExecutePostedVaa,
getManyProposalsInstructions,
getMultisigCluster, getMultisigCluster,
getProposals, getProposals,
isRemoteCluster,
MultisigInstruction, MultisigInstruction,
MultisigParser,
PRICE_FEED_MULTISIG, PRICE_FEED_MULTISIG,
PythMultisigInstruction,
UnrecognizedProgram,
UPGRADE_MULTISIG, UPGRADE_MULTISIG,
WormholeMultisigInstruction,
} from 'xc_admin_common' } from 'xc_admin_common'
import { ClusterContext } from '../contexts/ClusterContext' import { ClusterContext } from '../contexts/ClusterContext'
import { pythClusterApiUrls } from '../utils/pythClusterApiUrl' import { pythClusterApiUrls } from '../utils/pythClusterApiUrl'
interface MultisigHookData { export interface MultisigHookData {
isLoading: boolean isLoading: boolean
error: any // TODO: fix any error: any // TODO: fix any
proposeSquads: SquadsMesh | undefined proposeSquads: SquadsMesh | undefined
@ -39,6 +24,8 @@ interface MultisigHookData {
upgradeMultisigProposals: TransactionAccount[] upgradeMultisigProposals: TransactionAccount[]
priceFeedMultisigProposals: TransactionAccount[] priceFeedMultisigProposals: TransactionAccount[]
allProposalsIxsParsed: MultisigInstruction[][] allProposalsIxsParsed: MultisigInstruction[][]
connection?: Connection
refreshData?: () => { fetchData: () => Promise<void>; cancel: () => void }
setpriceFeedMultisigProposals: React.Dispatch< setpriceFeedMultisigProposals: React.Dispatch<
React.SetStateAction<TransactionAccount[]> React.SetStateAction<TransactionAccount[]>
> >
@ -52,7 +39,8 @@ const getSortedProposals = async (
return proposals.sort((a, b) => b.transactionIndex - a.transactionIndex) 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 { cluster } = useContext(ClusterContext)
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState(null) const [error, setError] = useState(null)
@ -69,14 +57,11 @@ export const useMultisig = (wallet: Wallet): MultisigHookData => {
const [allProposalsIxsParsed, setAllProposalsIxsParsed] = useState< const [allProposalsIxsParsed, setAllProposalsIxsParsed] = useState<
MultisigInstruction[][] MultisigInstruction[][]
>([]) >([])
const [proposeSquads, setProposeSquads] = useState<SquadsMesh>() const [squads, setSquads] = useState<SquadsMesh | undefined>()
const [voteSquads, setVoteSquads] = useState<SquadsMesh>()
const anchorWallet = useAnchorWallet()
const [urlsIndex, setUrlsIndex] = useState(0) const [urlsIndex, setUrlsIndex] = useState(0)
useEffect(() => { useEffect(() => {
setIsLoading(true)
setError(null) setError(null)
}, [urlsIndex, cluster]) }, [urlsIndex, cluster])
@ -84,39 +69,34 @@ export const useMultisig = (wallet: Wallet): MultisigHookData => {
setUrlsIndex(0) setUrlsIndex(0)
}, [cluster]) }, [cluster])
useEffect(() => { const multisigCluster = useMemo(() => getMultisigCluster(cluster), [cluster])
const urls = pythClusterApiUrls(getMultisigCluster(cluster))
const connection = new Connection(urls[urlsIndex].rpcUrl, { const connection = useMemo(() => {
const urls = pythClusterApiUrls(multisigCluster)
return new Connection(urls[urlsIndex].rpcUrl, {
commitment: 'confirmed', commitment: 'confirmed',
wsEndpoint: urls[urlsIndex].wsUrl, wsEndpoint: urls[urlsIndex].wsUrl,
}) })
}, [urlsIndex, multisigCluster])
useEffect(() => {
if (wallet) { if (wallet) {
setProposeSquads( setSquads(
new SquadsMesh({ new SquadsMesh({
connection, connection,
wallet, wallet,
}) })
) )
} else {
setSquads(undefined)
} }
if (anchorWallet) { }, [wallet, urlsIndex, cluster, connection])
setVoteSquads(
new SquadsMesh({
connection,
wallet: anchorWallet as Wallet,
})
)
}
}, [wallet, urlsIndex, cluster, anchorWallet])
useEffect(() => { const refreshData = useCallback(() => {
let cancelled = false 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 { try {
// mock wallet to allow users to view proposals without connecting their wallet // mock wallet to allow users to view proposals without connecting their wallet
const readOnlySquads = new SquadsMesh({ const readOnlySquads = new SquadsMesh({
@ -125,15 +105,13 @@ export const useMultisig = (wallet: Wallet): MultisigHookData => {
}) })
if (cancelled) return if (cancelled) return
setUpgradeMultisigAccount( setUpgradeMultisigAccount(
await readOnlySquads.getMultisig( await readOnlySquads.getMultisig(UPGRADE_MULTISIG[multisigCluster])
UPGRADE_MULTISIG[getMultisigCluster(cluster)]
)
) )
try { try {
if (cancelled) return if (cancelled) return
setpriceFeedMultisigAccount( setpriceFeedMultisigAccount(
await readOnlySquads.getMultisig( await readOnlySquads.getMultisig(
PRICE_FEED_MULTISIG[getMultisigCluster(cluster)] PRICE_FEED_MULTISIG[multisigCluster]
) )
) )
} catch (e) { } catch (e) {
@ -142,67 +120,18 @@ export const useMultisig = (wallet: Wallet): MultisigHookData => {
} }
if (cancelled) return if (cancelled) return
setUpgradeMultisigProposals( const upgradeProposals = await getSortedProposals(
await getSortedProposals( readOnlySquads,
readOnlySquads, UPGRADE_MULTISIG[multisigCluster]
UPGRADE_MULTISIG[getMultisigCluster(cluster)]
)
) )
setUpgradeMultisigProposals(upgradeProposals)
try { try {
if (cancelled) return if (cancelled) return
const sortedPriceFeedMultisigProposals = await getSortedProposals( const sortedPriceFeedMultisigProposals = await getSortedProposals(
readOnlySquads, readOnlySquads,
PRICE_FEED_MULTISIG[getMultisigCluster(cluster)] PRICE_FEED_MULTISIG[multisigCluster]
) )
const allProposalsIxs = await getManyProposalsInstructions( setpriceFeedMultisigProposals(sortedPriceFeedMultisigProposals)
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)
} catch (e) { } catch (e) {
console.error(e) console.error(e)
setAllProposalsIxsParsed([]) setAllProposalsIxsParsed([])
@ -213,6 +142,7 @@ export const useMultisig = (wallet: Wallet): MultisigHookData => {
} catch (e) { } catch (e) {
console.log(e) console.log(e)
if (cancelled) return if (cancelled) return
const urls = pythClusterApiUrls(multisigCluster)
if (urlsIndex === urls.length - 1) { if (urlsIndex === urls.length - 1) {
// @ts-ignore // @ts-ignore
setError(e) setError(e)
@ -225,23 +155,32 @@ export const useMultisig = (wallet: Wallet): MultisigHookData => {
) )
} }
} }
})() }
const cancel = () => {
return () => {
cancelled = true cancelled = true
} }
}, [urlsIndex, cluster])
return { cancel, fetchData }
}, [multisigCluster, urlsIndex, connection])
useEffect(() => {
const { cancel, fetchData } = refreshData()
fetchData()
return cancel
}, [refreshData])
return { return {
isLoading, isLoading,
error, error,
proposeSquads, proposeSquads: squads,
voteSquads, voteSquads: squads,
upgradeMultisigAccount, upgradeMultisigAccount,
priceFeedMultisigAccount, priceFeedMultisigAccount,
upgradeMultisigProposals, upgradeMultisigProposals,
priceFeedMultisigProposals, priceFeedMultisigProposals,
allProposalsIxsParsed, allProposalsIxsParsed,
refreshData,
connection,
setpriceFeedMultisigProposals, 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 { Tab } from '@headlessui/react'
import { useAnchorWallet } from '@solana/wallet-adapter-react'
import { Keypair } from '@solana/web3.js'
import * as fs from 'fs' import * as fs from 'fs'
import type { GetServerSideProps, NextPage } from 'next' import type { GetServerSideProps, NextPage } from 'next'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
@ -88,8 +84,6 @@ const Home: NextPage<{
}) => { }) => {
const [currentTabIndex, setCurrentTabIndex] = useState(0) const [currentTabIndex, setCurrentTabIndex] = useState(0)
const tabInfoArray = Object.values(TAB_INFO) const tabInfoArray = Object.values(TAB_INFO)
const anchorWallet = useAnchorWallet()
const wallet = anchorWallet as Wallet
const router = useRouter() const router = useRouter()
@ -123,8 +117,11 @@ const Home: NextPage<{
return ( return (
<Layout> <Layout>
<PythContextProvider> <PythContextProvider
<MultisigContextProvider wallet={wallet}> publisherKeyToNameMapping={publisherKeyToNameMapping}
multisigSignerKeyToNameMapping={multisigSignerKeyToNameMapping}
>
<MultisigContextProvider>
<div className="container relative pt-16 md:pt-20"> <div className="container relative pt-16 md:pt-20">
<div className="py-8 md:py-16"> <div className="py-8 md:py-16">
<Tab.Group <Tab.Group
@ -150,20 +147,17 @@ const Home: NextPage<{
</div> </div>
</div> </div>
{tabInfoArray[currentTabIndex].queryString === {tabInfoArray[currentTabIndex].queryString ===
TAB_INFO.General.queryString ? ( TAB_INFO.General.queryString && (
<General proposerServerUrl={proposerServerUrl} /> <General proposerServerUrl={proposerServerUrl} />
) : tabInfoArray[currentTabIndex].queryString === )}
TAB_INFO.UpdatePermissions.queryString ? ( {tabInfoArray[currentTabIndex].queryString ===
<UpdatePermissions /> TAB_INFO.UpdatePermissions.queryString && <UpdatePermissions />}
) : tabInfoArray[currentTabIndex].queryString === {tabInfoArray[currentTabIndex].queryString ===
TAB_INFO.Proposals.queryString ? ( TAB_INFO.Proposals.queryString && (
<StatusFilterProvider> <StatusFilterProvider>
<Proposals <Proposals />
publisherKeyToNameMapping={publisherKeyToNameMapping}
multisigSignerKeyToNameMapping={multisigSignerKeyToNameMapping}
/>
</StatusFilterProvider> </StatusFilterProvider>
) : null} )}
</MultisigContextProvider> </MultisigContextProvider>
</PythContextProvider> </PythContextProvider>
</Layout> </Layout>

View File

@ -278,13 +278,13 @@
} }
.dialogTitle { .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 { .action-btn {
@apply h-[45px] rounded-full bg-pythPurple px-8 font-mono font-semibold uppercase leading-none transition-colors hover:bg-mediumSlateBlue disabled:opacity-70 disabled:hover:bg-pythPurple; @apply h-[45px] rounded-full bg-pythPurple px-8 font-mono font-semibold uppercase leading-none transition-colors hover:bg-mediumSlateBlue disabled:opacity-70 disabled:hover:bg-pythPurple;
} }
.sub-action-btn { .sub-action-btn {
@apply h-[45px] rounded-full bg-darkGray2 px-8 font-mono font-semibold uppercase leading-none transition-colors hover:bg-darkGray4 disabled:opacity-70 disabled:hover:bg-darkGray2; @apply h-[45px] rounded-full bg-darkGray2 px-8 font-mono font-semibold uppercase leading-none transition-colors hover:bg-darkGray4 disabled:opacity-70 disabled:hover:bg-darkGray2;
} }