From f595d61ccdcb8e2f37de845ab0afbe08ad52b930 Mon Sep 17 00:00:00 2001 From: Mohammad Amin Khashkhashi Moghaddam Date: Thu, 20 Jul 2023 12:18:05 +0200 Subject: [PATCH] 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 --- .../InstructionViews/AccountUtils.tsx | 34 + .../WormholeInstructionView.tsx | 451 ++++++++ .../components/InstructionViews/utils.ts | 19 + .../components/tabs/General.tsx | 20 +- .../components/tabs/Proposals.tsx | 1008 +++++------------ .../contexts/MultisigContext.tsx | 32 +- .../contexts/PythContext.tsx | 59 +- .../xc_admin_frontend/hooks/useMultisig.ts | 155 +-- .../xc_admin_frontend/pages/index.tsx | 32 +- .../xc_admin_frontend/styles/globals.css | 6 +- 10 files changed, 943 insertions(+), 873 deletions(-) create mode 100644 governance/xc_admin/packages/xc_admin_frontend/components/InstructionViews/AccountUtils.tsx create mode 100644 governance/xc_admin/packages/xc_admin_frontend/components/InstructionViews/WormholeInstructionView.tsx create mode 100644 governance/xc_admin/packages/xc_admin_frontend/components/InstructionViews/utils.ts diff --git a/governance/xc_admin/packages/xc_admin_frontend/components/InstructionViews/AccountUtils.tsx b/governance/xc_admin/packages/xc_admin_frontend/components/InstructionViews/AccountUtils.tsx new file mode 100644 index 00000000..5bb0d88a --- /dev/null +++ b/governance/xc_admin/packages/xc_admin_frontend/components/InstructionViews/AccountUtils.tsx @@ -0,0 +1,34 @@ +export const SignerTag = () => { + return ( +
+ Signer +
+ ) +} + +export const WritableTag = () => { + return ( +
+ Writable +
+ ) +} + +export const ParsedAccountPubkeyRow = ({ + mapping, + title, + pubkey, +}: { + mapping: { [key: string]: string } + title: string + pubkey: string +}) => { + return ( +
+
+ ⤷ {title} +
+
{mapping[pubkey]}
+
+ ) +} diff --git a/governance/xc_admin/packages/xc_admin_frontend/components/InstructionViews/WormholeInstructionView.tsx b/governance/xc_admin/packages/xc_admin_frontend/components/InstructionViews/WormholeInstructionView.tsx new file mode 100644 index 00000000..d8b666c7 --- /dev/null +++ b/governance/xc_admin/packages/xc_admin_frontend/components/InstructionViews/WormholeInstructionView.tsx @@ -0,0 +1,451 @@ +import { + AptosAuthorizeUpgradeContract, + AuthorizeGovernanceDataSourceTransfer, + CosmosUpgradeContract, + EvmSetWormholeAddress, + EvmUpgradeContract, + ExecutePostedVaa, + MessageBufferMultisigInstruction, + MultisigParser, + PythGovernanceAction, + PythMultisigInstruction, + RequestGovernanceDataSourceTransfer, + SetDataSources, + SetFee, + SetValidPeriod, + UnrecognizedProgram, + WormholeMultisigInstruction, +} from 'xc_admin_common' +import { AccountMeta, PublicKey } from '@solana/web3.js' +import CopyPubkey from '../common/CopyPubkey' +import { useContext } from 'react' +import { ClusterContext } from '../../contexts/ClusterContext' +import { ParsedAccountPubkeyRow, SignerTag, WritableTag } from './AccountUtils' +import { usePythContext } from '../../contexts/PythContext' + +import { getMappingCluster, isPubkey } from './utils' + +const GovernanceInstructionView = ({ + instruction, + actionName, + content, +}: { + instruction: PythGovernanceAction + actionName: string + content: JSX.Element +}) => { + return ( +
+
Action: {actionName}
+
Chain Id: {instruction.targetChainId}
+ {content} +
+ Raw payload hex:{' '} + +
+
+ ) +} +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 ( +
+

Wormhole Instructions

+
+ {!governanceAction && ( + <> +
Unknown message
+
Raw hex payload:
+
{(instruction.args.payload as Buffer).toString('hex')}
+ + )} + {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 ( + <> +
+
Program
+
+ {parsedInstruction instanceof PythMultisigInstruction + ? 'Pyth Oracle' + : parsedInstruction instanceof WormholeMultisigInstruction + ? 'Wormhole' + : parsedInstruction instanceof + MessageBufferMultisigInstruction + ? 'Message Buffer' + : 'Unknown'} +
+
+
+
Instruction Name
+
+ {parsedInstruction instanceof PythMultisigInstruction || + parsedInstruction instanceof WormholeMultisigInstruction || + parsedInstruction instanceof MessageBufferMultisigInstruction + ? parsedInstruction.name + : 'Unknown'} +
+
+
+
Arguments
+ {parsedInstruction instanceof PythMultisigInstruction || + parsedInstruction instanceof WormholeMultisigInstruction || + parsedInstruction instanceof + MessageBufferMultisigInstruction ? ( + Object.keys(parsedInstruction.args).length > 0 ? ( +
+
+
Key
+
Value
+
+ {Object.keys(parsedInstruction.args).map((key, index) => ( + <> +
+
{key}
+ {parsedInstruction.args[key] instanceof + PublicKey ? ( + + ) : typeof instruction.args[key] === 'string' && + isPubkey(instruction.args[key]) ? ( + + ) : ( +
+ {typeof parsedInstruction.args[key] === 'string' + ? parsedInstruction.args[key] + : parsedInstruction.args[key] instanceof + Uint8Array + ? parsedInstruction.args[key].toString('hex') + : JSON.stringify(parsedInstruction.args[key])} +
+ )} +
+ {key === 'pub' && + parsedInstruction.args[key].toBase58() in + publisherKeyToNameMappingCluster ? ( + + ) : null} + + ))} +
+ ) : ( +
No arguments
+ ) + ) : ( +
Unknown
+ )} +
+ {parsedInstruction instanceof PythMultisigInstruction || + parsedInstruction instanceof WormholeMultisigInstruction || + parsedInstruction instanceof MessageBufferMultisigInstruction ? ( +
+
Accounts
+ {Object.keys(parsedInstruction.accounts.named).length > 0 ? ( +
+
+
Account
+
Pubkey
+
+ {Object.keys(parsedInstruction.accounts.named).map( + (key, index) => ( + <> +
+
+ {key} +
+
+
+ {parsedInstruction.accounts.named[key] + .isSigner ? ( + + ) : null} + {parsedInstruction.accounts.named[key] + .isWritable ? ( + + ) : null} +
+ +
+
+ {key === 'priceAccount' && + parsedInstruction.accounts.named[ + key + ].pubkey.toBase58() in + priceAccountKeyToSymbolMapping ? ( + + ) : key === 'productAccount' && + parsedInstruction.accounts.named[ + key + ].pubkey.toBase58() in + productAccountKeyToSymbolMapping ? ( + + ) : null} + + ) + )} + {parsedInstruction.accounts.remaining.map( + (accountMeta, index) => ( + <> +
+
+ Remaining {index + 1} +
+
+
+ {accountMeta.isSigner ? : null} + {accountMeta.isWritable ? ( + + ) : null} +
+ +
+
+ + ) + )} +
+ ) : ( +
No accounts
+ )} +
+ ) : parsedInstruction instanceof UnrecognizedProgram ? ( + <> +
+
Program ID
+
+ {parsedInstruction.instruction.programId.toBase58()} +
+
+
+
Data
+
+ {parsedInstruction.instruction.data.length > 0 + ? parsedInstruction.instruction.data.toString('hex') + : 'No data'} +
+
+
+
Keys
+
+
+
Key #
+
Pubkey
+
+ {parsedInstruction.instruction.keys.map((key, index) => ( + <> +
+
Key {index + 1}
+
+ {key.isSigner ? : null} + {key.isWritable ? : null} + +
+
+ + ))} +
+
+ + ) : null} + + ) + })} + {governanceAction instanceof EvmUpgradeContract && ( + + Address: + +
+ } + /> + )} + + {governanceAction instanceof CosmosUpgradeContract && ( + Code id:{governanceAction.codeId.toString()}} + /> + )} + + {governanceAction instanceof AptosAuthorizeUpgradeContract && ( + + Package hash: + + + } + /> + )} + + {governanceAction instanceof SetFee && ( + +
+ New Fee Value: {governanceAction.newFeeValue.toString()} +
+
New Fee Expo: {governanceAction.newFeeExpo.toString()}
+ + } + /> + )} + {governanceAction instanceof SetDataSources && ( + + {governanceAction.dataSources.map((dataSource, idx) => ( +
+ Datasource #{idx + 1}: +
    +
  • Emitter Chain: {dataSource.emitterChain}
  • +
  • + Emitter Address:{' '} + +
  • +
+
+ ))} + + } + /> + )} + + {governanceAction instanceof EvmSetWormholeAddress && ( + + New Wormhole Address: + + + } + /> + )} + + {governanceAction instanceof SetValidPeriod && ( + + New Valid Period: {governanceAction.newValidPeriod.toString()} + + } + /> + )} + + {governanceAction instanceof RequestGovernanceDataSourceTransfer && ( + + Governance Data Source Index:{' '} + {governanceAction.governanceDataSourceIndex} + + } + /> + )} + + {governanceAction instanceof AuthorizeGovernanceDataSourceTransfer && ( + + Claim Vaa hex:{' '} + + + } + /> + )} + + ) +} diff --git a/governance/xc_admin/packages/xc_admin_frontend/components/InstructionViews/utils.ts b/governance/xc_admin/packages/xc_admin_frontend/components/InstructionViews/utils.ts new file mode 100644 index 00000000..86238dcd --- /dev/null +++ b/governance/xc_admin/packages/xc_admin_frontend/components/InstructionViews/utils.ts @@ -0,0 +1,19 @@ +import { PublicKey } from '@solana/web3.js' + +export const getMappingCluster = (cluster: string) => { + if (cluster === 'mainnet-beta' || cluster === 'pythnet') { + return 'pythnet' + } else { + return 'pythtest' + } +} + +// check if a string is a pubkey +export const isPubkey = (str: string) => { + try { + new PublicKey(str) + return true + } catch (e) { + return false + } +} diff --git a/governance/xc_admin/packages/xc_admin_frontend/components/tabs/General.tsx b/governance/xc_admin/packages/xc_admin_frontend/components/tabs/General.tsx index 7a6a1ae5..675e31cf 100644 --- a/governance/xc_admin/packages/xc_admin_frontend/components/tabs/General.tsx +++ b/governance/xc_admin/packages/xc_admin_frontend/components/tabs/General.tsx @@ -778,14 +778,18 @@ const General = ({ proposerServerUrl }: { proposerServerUrl: string }) => { ) : (

No proposed changes.

)} - {Object.keys(changes).length > 0 ? ( - - ) : null} + {Object.keys(changes).length > 0 && ( + <> + + {!proposeSquads &&
Please connect your wallet
} + + )} ) } diff --git a/governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals.tsx b/governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals.tsx index 95524a30..8ceebf4a 100644 --- a/governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals.tsx +++ b/governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals.tsx @@ -1,10 +1,11 @@ import * as Tooltip from '@radix-ui/react-tooltip' import { useWallet } from '@solana/wallet-adapter-react' -import { AccountMeta, PublicKey } from '@solana/web3.js' +import { AccountMeta, Keypair, PublicKey } from '@solana/web3.js' import { MultisigAccount, TransactionAccount } from '@sqds/mesh/lib/types' import { useRouter } from 'next/router' import { Dispatch, + Fragment, SetStateAction, useCallback, useContext, @@ -24,12 +25,15 @@ import { MessageBufferMultisigInstruction, UnrecognizedProgram, WormholeMultisigInstruction, + getManyProposalsInstructions, + UPGRADE_MULTISIG, } from 'xc_admin_common' import { ClusterContext } from '../../contexts/ClusterContext' import { useMultisigContext } from '../../contexts/MultisigContext' import { usePythContext } from '../../contexts/PythContext' import { StatusFilterContext } from '../../contexts/StatusFilterContext' import VerifiedIcon from '../../images/icons/verified.inline.svg' +import WarningIcon from '../../images/icons/warning.inline.svg' import VotedIcon from '../../images/icons/voted.inline.svg' import { capitalizeFirstLetter } from '../../utils/capitalizeFirstLetter' import ClusterSwitch from '../ClusterSwitch' @@ -37,35 +41,22 @@ import CopyPubkey from '../common/CopyPubkey' import Spinner from '../common/Spinner' import Loadbar from '../loaders/Loadbar' import ProposalStatusFilter from '../ProposalStatusFilter' +import SquadsMesh from '@sqds/mesh' +import NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet' +import { WormholeInstructionView } from '../InstructionViews/WormholeInstructionView' +import { + ParsedAccountPubkeyRow, + SignerTag, + WritableTag, +} from '../InstructionViews/AccountUtils' -// check if a string is a pubkey -const isPubkey = (str: string) => { - try { - new PublicKey(str) - return true - } catch (e) { - return false - } -} - -const getMappingCluster = (cluster: string) => { - if (cluster === 'mainnet-beta' || cluster === 'pythnet') { - return 'pythnet' - } else { - return 'pythtest' - } -} - +import { getMappingCluster, isPubkey } from '../InstructionViews/utils' const ProposalRow = ({ proposal, - verified, - voted, setCurrentProposalPubkey, multisig, }: { proposal: TransactionAccount - verified: boolean - voted: boolean setCurrentProposalPubkey: Dispatch> multisig: MultisigAccount | undefined }) => { @@ -105,10 +96,6 @@ const ProposalRow = ({ '...' + proposal.publicKey.toBase58().slice(-6)} {' '} -
- {verified ? : null} -
- {voted ? : null}
@@ -118,22 +105,6 @@ const ProposalRow = ({ ) } -const SignerTag = () => { - return ( -
- Signer -
- ) -} - -const WritableTag = () => { - return ( -
- Writable -
- ) -} - const StatusTag = ({ proposalStatus }: { proposalStatus: string }) => { return (
{ ) } -const VerifiedIconWithTooltip = () => { +const IconWithTooltip = ({ + icon, + tooltipText, +}: { + icon: JSX.Element + tooltipText: string +}) => { return (
- - - + {icon} - The instructions in this proposal are verified. + {tooltipText} @@ -175,46 +150,35 @@ const VerifiedIconWithTooltip = () => { ) } +const VerifiedIconWithTooltip = () => { + return ( + } + tooltipText="The instructions in this proposal are verified." + /> + ) +} + +const UnverifiedIconWithTooltip = () => { + return ( + } + tooltipText="Be careful! The instructions in this proposal are not verified." + /> + ) +} + const VotedIconWithTooltip = () => { return ( -
- - - - - - - - You have voted on this proposal. - - - - -
- ) -} - -const ParsedAccountPubkeyRow = ({ - mapping, - title, - pubkey, -}: { - mapping: { [key: string]: string } - title: string - pubkey: string -}) => { - return ( -
-
- ⤷ {title} -
-
{mapping[pubkey]}
-
+ } + tooltipText=" You have voted on this proposal." + /> ) } const getProposalStatus = ( - proposal: TransactionAccount | ClientProposal | undefined, + proposal: TransactionAccount | undefined, multisig: MultisigAccount | undefined ): string => { if (multisig && proposal) { @@ -228,61 +192,74 @@ const getProposalStatus = ( } } +const AccountList = ({ + listName, + accounts, +}: { + listName: string + accounts: PublicKey[] +}) => { + const { multisigSignerKeyToNameMapping } = usePythContext() + return ( +
+

+ {listName}: {accounts.length} +

+
+ {accounts.map((pubkey, idx) => ( +
+
+
+ Key {idx + 1}{' '} + {pubkey.toBase58() in multisigSignerKeyToNameMapping && + `(${multisigSignerKeyToNameMapping[pubkey.toBase58()]})`} +
+ +
+
+ ))} +
+ ) +} + +type ProposalType = 'priceFeed' | 'governance' + const Proposal = ({ - publisherKeyToNameMapping, - multisigSignerKeyToNameMapping, proposal, proposalIndex, - instructions, - verified, multisig, + proposalType, }: { - publisherKeyToNameMapping: Record> - multisigSignerKeyToNameMapping: Record proposal: TransactionAccount | undefined proposalIndex: number - instructions: MultisigInstruction[] - verified: boolean multisig: MultisigAccount | undefined + proposalType: ProposalType }) => { const [currentProposal, setCurrentProposal] = useState() + const [instructions, setInstructions] = useState([]) const [isTransactionLoading, setIsTransactionLoading] = useState(false) - const [ - productAccountKeyToSymbolMapping, - setProductAccountKeyToSymbolMapping, - ] = useState<{ [key: string]: string }>({}) - const [priceAccountKeyToSymbolMapping, setPriceAccountKeyToSymbolMapping] = - useState<{ [key: string]: string }>({}) const { cluster } = useContext(ClusterContext) - const publisherKeyToNameMappingCluster = - publisherKeyToNameMapping[getMappingCluster(cluster)] + const { voteSquads, isLoading: isMultisigLoading, setpriceFeedMultisigProposals, + connection, } = useMultisigContext() - const { rawConfig, dataIsLoading } = usePythContext() + const { + priceAccountKeyToSymbolMapping, + productAccountKeyToSymbolMapping, + publisherKeyToNameMapping, + multisigSignerKeyToNameMapping, + } = usePythContext() + const publisherKeyToNameMappingCluster = + publisherKeyToNameMapping[getMappingCluster(cluster)] + const { publicKey: signerPublicKey } = useWallet() useEffect(() => { setCurrentProposal(proposal) }, [proposal]) - useEffect(() => { - 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 - }) - ) - setProductAccountKeyToSymbolMapping(productAccountMapping) - setPriceAccountKeyToSymbolMapping(priceAccountMapping) - } - }, [rawConfig, dataIsLoading]) - const proposalStatus = getProposalStatus(proposal, multisig) useEffect(() => { @@ -295,14 +272,87 @@ const Proposal = ({ } }, [currentProposal, setpriceFeedMultisigProposals, proposalIndex]) - const handleClickApprove = async () => { + const verified = + currentProposal && + Object.keys(currentProposal.status)[0] !== 'draft' && + instructions.length > 0 && + instructions.every( + (ix) => + ix instanceof PythMultisigInstruction || + (ix instanceof WormholeMultisigInstruction && + ix.name === 'postMessage' && + ix.governanceAction instanceof ExecutePostedVaa && + ix.governanceAction.instructions.every((remoteIx) => { + const innerMultisigParser = MultisigParser.fromCluster(cluster) + const parsedRemoteInstruction = + innerMultisigParser.parseInstruction({ + programId: remoteIx.programId, + data: remoteIx.data as Buffer, + keys: remoteIx.keys as AccountMeta[], + }) + return ( + parsedRemoteInstruction instanceof PythMultisigInstruction || + parsedRemoteInstruction instanceof + MessageBufferMultisigInstruction + ) + }) && + ix.governanceAction.targetChainId === 'pythnet') + ) + + const voted = + currentProposal && + signerPublicKey && + (currentProposal.approved.some( + (p) => p.toBase58() === signerPublicKey.toBase58() + ) || + currentProposal.cancelled.some( + (p) => p.toBase58() === signerPublicKey.toBase58() + ) || + currentProposal.rejected.some( + (p) => p.toBase58() === signerPublicKey.toBase58() + )) + + useEffect(() => { + const fetchInstructions = async () => { + if (currentProposal && connection) { + const readOnlySquads = new SquadsMesh({ + connection, + wallet: new NodeWallet(new Keypair()), + }) + const proposalInstructions = ( + await getManyProposalsInstructions(readOnlySquads, [currentProposal]) + )[0] + const multisigParser = MultisigParser.fromCluster( + getMultisigCluster(cluster) + ) + const parsedInstructions = proposalInstructions.map((ix) => + multisigParser.parseInstruction({ + programId: ix.programId, + data: ix.data as Buffer, + keys: ix.keys as AccountMeta[], + }) + ) + setInstructions(parsedInstructions) + } else { + setInstructions([]) + } + } + fetchInstructions().catch(console.error) + }, [cluster, currentProposal, voteSquads, connection]) + + const handleClick = async ( + handler: (squad: SquadsMesh, proposalKey: PublicKey) => any, + msg: string + ) => { if (proposal && voteSquads) { try { setIsTransactionLoading(true) - await voteSquads.approveTransaction(proposal.publicKey) + await handler(voteSquads, proposal.publicKey) const proposals = await getProposals( voteSquads, - PRICE_FEED_MULTISIG[getMultisigCluster(cluster)] + proposalType === 'priceFeed' + ? PRICE_FEED_MULTISIG[getMultisigCluster(cluster)] + : UPGRADE_MULTISIG[getMultisigCluster(cluster)] ) setCurrentProposal( proposals.find( @@ -311,88 +361,37 @@ const Proposal = ({ currentProposal?.publicKey.toBase58() ) ) - toast.success(`Approved proposal ${proposal.publicKey.toBase58()}`) - setIsTransactionLoading(false) + toast.success(msg) } catch (e: any) { - setIsTransactionLoading(false) toast.error(capitalizeFirstLetter(e.message)) + } finally { + setIsTransactionLoading(false) } } } + const handleClickApprove = async () => { + await handleClick(async (squad: SquadsMesh, proposalKey: PublicKey) => { + await squad.approveTransaction(proposalKey) + }, `Approved proposal ${proposal?.publicKey.toBase58()}`) + } + const handleClickReject = async () => { - if (proposal && voteSquads) { - try { - setIsTransactionLoading(true) - await voteSquads.rejectTransaction(proposal.publicKey) - const proposals = await getProposals( - voteSquads, - PRICE_FEED_MULTISIG[getMultisigCluster(cluster)] - ) - setCurrentProposal( - proposals.find( - (proposal) => - proposal.publicKey.toBase58() === - currentProposal?.publicKey.toBase58() - ) - ) - toast.success(`Rejected proposal ${proposal.publicKey.toBase58()}`) - setIsTransactionLoading(false) - } catch (e: any) { - setIsTransactionLoading(false) - toast.error(capitalizeFirstLetter(e.message)) - } - } + await handleClick(async (squad: SquadsMesh, proposalKey: PublicKey) => { + await squad.rejectTransaction(proposalKey) + }, `Rejected proposal ${proposal?.publicKey.toBase58()}`) } const handleClickExecute = async () => { - if (proposal && voteSquads) { - try { - setIsTransactionLoading(true) - await voteSquads.executeTransaction(proposal.publicKey) - const proposals = await getProposals( - voteSquads, - PRICE_FEED_MULTISIG[getMultisigCluster(cluster)] - ) - setCurrentProposal( - proposals.find( - (proposal) => - proposal.publicKey.toBase58() === - currentProposal?.publicKey.toBase58() - ) - ) - toast.success(`Executed proposal ${proposal.publicKey.toBase58()}`) - setIsTransactionLoading(false) - } catch (e: any) { - setIsTransactionLoading(false) - toast.error(capitalizeFirstLetter(e.message)) - } - } + await handleClick(async (squad: SquadsMesh, proposalKey: PublicKey) => { + await squad.executeTransaction(proposalKey) + }, `Executed proposal ${proposal?.publicKey.toBase58()}`) } const handleClickCancel = async () => { - if (proposal && voteSquads) { - try { - setIsTransactionLoading(true) - await voteSquads.cancelTransaction(proposal.publicKey) - const proposals = await getProposals( - voteSquads, - PRICE_FEED_MULTISIG[getMultisigCluster(cluster)] - ) - setCurrentProposal( - proposals.find( - (proposal) => - proposal.publicKey.toBase58() === - currentProposal?.publicKey.toBase58() - ) - ) - toast.success(`Cancelled proposal ${proposal.publicKey.toBase58()}`) - setIsTransactionLoading(false) - } catch (e: any) { - setIsTransactionLoading(false) - toast.error(capitalizeFirstLetter(e.message)) - } - } + await handleClick(async (squad: SquadsMesh, proposalKey: PublicKey) => { + await squad.cancelTransaction(proposalKey) + }, `Cancelled proposal ${proposal?.publicKey.toBase58()}`) } return currentProposal !== undefined && @@ -402,7 +401,14 @@ const Proposal = ({

Info

- {verified ? : null} +
+ {verified ? ( + + ) : ( + + )} + {voted && } +

@@ -451,12 +457,14 @@ const Proposal = ({ {proposalStatus === 'active' ? (
) : null}
- {currentProposal.approved.length > 0 ? ( -
-

- Confirmed: {currentProposal.approved.length} -

-
- {currentProposal.approved.map((pubkey, idx) => ( - <> -
-
- Key {idx + 1}{' '} - {pubkey.toBase58() in multisigSignerKeyToNameMapping - ? `(${multisigSignerKeyToNameMapping[pubkey.toBase58()]})` - : null} -
- -
- - ))} -
- ) : null} - {currentProposal.rejected.length > 0 ? ( -
-

- Rejected: {currentProposal.rejected.length} -

-
- {currentProposal.rejected.map((pubkey, idx) => ( - <> -
-
- Key {idx + 1}{' '} - {pubkey.toBase58() in multisigSignerKeyToNameMapping - ? `(${multisigSignerKeyToNameMapping[pubkey.toBase58()]})` - : null} -
- -
- - ))} -
- ) : null} - {currentProposal.cancelled.length > 0 ? ( -
-

- Cancelled: {currentProposal.cancelled.length} -

-
- {currentProposal.cancelled.map((pubkey, idx) => ( -
-
- Key {idx + 1}{' '} - {pubkey.toBase58() in multisigSignerKeyToNameMapping - ? `(${multisigSignerKeyToNameMapping[pubkey.toBase58()]})` - : null} -
- -
- ))} -
- ) : null} + {currentProposal.approved.length > 0 && ( + + )} + {currentProposal.rejected.length > 0 && ( + + )} + {currentProposal.cancelled.length > 0 && ( + + )}

Total Instructions: {instructions.length}


{instructions?.map((instruction, index) => ( - <> +

Instruction {index + 1}

@@ -582,7 +543,7 @@ const Proposal = ({ className="flex justify-between" >
Target Chain
-
{cluster}
+
{instruction.governanceAction.targetChainId}
) : null} @@ -764,343 +725,14 @@ const Proposal = ({
) : null} - {instruction instanceof WormholeMultisigInstruction ? ( -
-

Wormhole Instructions

-
- {instruction.governanceAction instanceof ExecutePostedVaa - ? instruction.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 ( - <> -
-
Program
-
- {parsedInstruction instanceof - PythMultisigInstruction - ? 'Pyth Oracle' - : parsedInstruction instanceof - WormholeMultisigInstruction - ? 'Wormhole' - : parsedInstruction instanceof - MessageBufferMultisigInstruction - ? 'Message Buffer' - : 'Unknown'} -
-
-
-
Instruction Name
-
- {parsedInstruction instanceof - PythMultisigInstruction || - parsedInstruction instanceof - WormholeMultisigInstruction || - parsedInstruction instanceof - MessageBufferMultisigInstruction - ? parsedInstruction.name - : 'Unknown'} -
-
-
-
Arguments
- {parsedInstruction instanceof - PythMultisigInstruction || - parsedInstruction instanceof - WormholeMultisigInstruction || - parsedInstruction instanceof - MessageBufferMultisigInstruction ? ( - Object.keys(parsedInstruction.args).length > - 0 ? ( -
-
-
Key
-
Value
-
- {Object.keys(parsedInstruction.args).map( - (key, index) => ( - <> -
-
{key}
- {parsedInstruction.args[ - key - ] instanceof PublicKey ? ( - - ) : typeof instruction.args[key] === - 'string' && - isPubkey( - instruction.args[key] - ) ? ( - - ) : ( -
- {typeof parsedInstruction.args[ - key - ] === 'string' - ? parsedInstruction.args[key] - : parsedInstruction.args[ - key - ] instanceof Uint8Array - ? parsedInstruction.args[ - key - ].toString('hex') - : JSON.stringify( - parsedInstruction.args[ - key - ] - )} -
- )} -
- {key === 'pub' && - parsedInstruction.args[ - key - ].toBase58() in - publisherKeyToNameMappingCluster ? ( - - ) : null} - - ) - )} -
- ) : ( -
- No arguments -
- ) - ) : ( -
- Unknown -
- )} -
- {parsedInstruction instanceof - PythMultisigInstruction || - parsedInstruction instanceof - WormholeMultisigInstruction || - parsedInstruction instanceof - MessageBufferMultisigInstruction ? ( -
-
Accounts
- {Object.keys(parsedInstruction.accounts.named) - .length > 0 ? ( -
-
-
Account
-
Pubkey
-
- {Object.keys( - parsedInstruction.accounts.named - ).map((key, index) => ( - <> -
-
- {key} -
-
-
- {parsedInstruction.accounts.named[ - key - ].isSigner ? ( - - ) : null} - {parsedInstruction.accounts.named[ - key - ].isWritable ? ( - - ) : null} -
- -
-
- {key === 'priceAccount' && - parsedInstruction.accounts.named[ - key - ].pubkey.toBase58() in - priceAccountKeyToSymbolMapping ? ( - - ) : key === 'productAccount' && - parsedInstruction.accounts.named[ - key - ].pubkey.toBase58() in - productAccountKeyToSymbolMapping ? ( - - ) : null} - - ))} - {parsedInstruction.accounts.remaining.map( - (accountMeta, index) => ( - <> -
-
- Remaining {index + 1} -
-
-
- {accountMeta.isSigner ? ( - - ) : null} - {accountMeta.isWritable ? ( - - ) : null} -
- -
-
- - ) - )} -
- ) : ( -
No accounts
- )} -
- ) : parsedInstruction instanceof - UnrecognizedProgram ? ( - <> -
-
Program ID
-
- {parsedInstruction.instruction.programId.toBase58()} -
-
-
-
Data
-
- {parsedInstruction.instruction.data.length > - 0 - ? parsedInstruction.instruction.data.toString( - 'hex' - ) - : 'No data'} -
-
-
-
Keys
-
-
-
Key #
-
Pubkey
-
- {parsedInstruction.instruction.keys.map( - (key, index) => ( - <> -
-
Key {index + 1}
-
- {key.isSigner ? ( - - ) : null} - {key.isWritable ? ( - - ) : null} - -
-
- - ) - )} -
-
- - ) : null} - - ) - } - ) - : ''} -
- ) : null} + {instruction instanceof WormholeMultisigInstruction && ( + + )} {index !== instructions.length - 1 ? (
) : null} - + ))}
@@ -1111,80 +743,39 @@ const Proposal = ({ ) } -type ClientProposal = TransactionAccount & { verified: boolean; voted: boolean } - -const Proposals = ({ - publisherKeyToNameMapping, - multisigSignerKeyToNameMapping, -}: { - publisherKeyToNameMapping: Record> - multisigSignerKeyToNameMapping: Record -}) => { +const Proposals = () => { const router = useRouter() const { connected, publicKey: signerPublicKey } = useWallet() const [currentProposal, setCurrentProposal] = useState() const [currentProposalIndex, setCurrentProposalIndex] = useState() - const [allProposalsVerifiedArr, setAllProposalsVerifiedArr] = useState< - boolean[] - >([]) - const [proposalsVotedArr, setProposalsVotedArr] = useState([]) const [currentProposalPubkey, setCurrentProposalPubkey] = useState() const { cluster } = useContext(ClusterContext) const { statusFilter } = useContext(StatusFilterContext) + const { + upgradeMultisigAccount, priceFeedMultisigAccount, priceFeedMultisigProposals, - allProposalsIxsParsed, + upgradeMultisigProposals, isLoading: isMultisigLoading, + refreshData, } = useMultisigContext() - const [filteredProposals, setFilteredProposals] = useState( - [] - ) - 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(cluster) - const parsedRemoteInstruction = - innerMultisigParser.parseInstruction({ - programId: remoteIx.programId, - data: remoteIx.data as Buffer, - keys: remoteIx.keys as AccountMeta[], - }) - return ( - parsedRemoteInstruction instanceof - PythMultisigInstruction || - parsedRemoteInstruction instanceof - MessageBufferMultisigInstruction - ) - }) && - ix.governanceAction.targetChainId === 'pythnet') - ) && - Object.keys(priceFeedMultisigProposals[idx].status)[0] !== 'draft' + const [proposalType, setProposalType] = useState('priceFeed') - res.push(isAllIxsVerified) - }) - setAllProposalsVerifiedArr(res) - } - }, [ - allProposalsIxsParsed, - isMultisigLoading, - cluster, - priceFeedMultisigProposals, - ]) + const multisigAccount = + proposalType === 'priceFeed' + ? priceFeedMultisigAccount + : upgradeMultisigAccount + const multisigProposals = + proposalType === 'priceFeed' + ? priceFeedMultisigProposals + : upgradeMultisigProposals + const [filteredProposals, setFilteredProposals] = useState< + TransactionAccount[] + >([]) - const handleClickBackToPriceFeeds = () => { + const handleClickBackToProposals = () => { delete router.query.proposal router.push( { @@ -1202,86 +793,63 @@ const Proposals = ({ } }, [router.query.proposal]) + const switchProposalType = useCallback(() => { + if (proposalType === 'priceFeed') { + setProposalType('governance') + } else { + setProposalType('priceFeed') + } + }, [proposalType]) + useEffect(() => { if (currentProposalPubkey) { - const currProposal = priceFeedMultisigProposals.find( + const currProposal = multisigProposals.find( (proposal) => proposal.publicKey.toBase58() === currentProposalPubkey ) - const currProposalIndex = priceFeedMultisigProposals.findIndex( + const currProposalIndex = multisigProposals.findIndex( (proposal) => proposal.publicKey.toBase58() === currentProposalPubkey ) setCurrentProposal(currProposal) setCurrentProposalIndex( currProposalIndex === -1 ? undefined : currProposalIndex ) + if (currProposalIndex === -1) { + const otherProposals = + proposalType !== 'priceFeed' + ? priceFeedMultisigProposals + : upgradeMultisigProposals + if ( + otherProposals.findIndex( + (proposal) => + proposal.publicKey.toBase58() === currentProposalPubkey + ) !== -1 + ) { + switchProposalType() + } + } } - }, [ - currentProposalPubkey, - priceFeedMultisigProposals, - allProposalsIxsParsed, - cluster, - ]) + }, [currentProposalPubkey, multisigProposals, cluster]) useEffect(() => { - const allClientProposals = priceFeedMultisigProposals.map( - (proposal, idx) => ({ - ...proposal, - verified: allProposalsVerifiedArr[idx], - voted: proposalsVotedArr[idx], - }) - ) // filter price feed multisig proposals by status if (statusFilter === 'all') { - // pass priceFeedMultisigProposals and add verified and voted props - setFilteredProposals(allClientProposals) + setFilteredProposals(multisigProposals) } else { setFilteredProposals( - allClientProposals.filter( + multisigProposals.filter( (proposal) => - getProposalStatus(proposal, priceFeedMultisigAccount) === - statusFilter + getProposalStatus(proposal, multisigAccount) === statusFilter ) ) } - }, [ - statusFilter, - priceFeedMultisigAccount, - priceFeedMultisigProposals, - allProposalsVerifiedArr, - proposalsVotedArr, - ]) - - useEffect(() => { - if (priceFeedMultisigAccount && connected && signerPublicKey) { - const res: boolean[] = [] - priceFeedMultisigProposals.map((proposal) => { - // check if proposal.approved, proposal.cancelled, proposal.rejected has wallet pubkey and return true if anyone of them has wallet pubkey - const isProposalVoted = - proposal.approved.some( - (p) => p.toBase58() === signerPublicKey.toBase58() - ) || - proposal.cancelled.some( - (p) => p.toBase58() === signerPublicKey.toBase58() - ) || - proposal.rejected.some( - (p) => p.toBase58() === signerPublicKey.toBase58() - ) - res.push(isProposalVoted) - }) - setProposalsVotedArr(res) - } - }, [ - priceFeedMultisigAccount, - priceFeedMultisigProposals, - connected, - signerPublicKey, - ]) + }, [statusFilter, multisigAccount, multisigProposals]) return (

+ {proposalType === 'priceFeed' ? 'Price Feed ' : 'Governance '}{' '} {router.query.proposal === undefined ? 'Proposals' : 'Proposal'}

@@ -1289,10 +857,35 @@ const Proposals = ({
{router.query.proposal === undefined ? ( <> -
-
+
+
+
+ {refreshData && ( + + )} + +
{isMultisigLoading ? ( @@ -1311,19 +904,17 @@ const Proposals = ({
{filteredProposals.map((proposal, idx) => ( ))}
) : (
- No proposals found. If you're a member of the price - feed multisig, you can create a proposal. + No proposals found. If you're a member of the + multisig, you can create a proposal.
)} @@ -1334,19 +925,16 @@ const Proposals = ({ <>
← back to proposals
diff --git a/governance/xc_admin/packages/xc_admin_frontend/contexts/MultisigContext.tsx b/governance/xc_admin/packages/xc_admin_frontend/contexts/MultisigContext.tsx index 39cd12ff..d33cb3e7 100644 --- a/governance/xc_admin/packages/xc_admin_frontend/contexts/MultisigContext.tsx +++ b/governance/xc_admin/packages/xc_admin_frontend/contexts/MultisigContext.tsx @@ -1,25 +1,10 @@ -import { Wallet } from '@coral-xyz/anchor' import SquadsMesh from '@sqds/mesh' import { MultisigAccount, TransactionAccount } from '@sqds/mesh/lib/types' import React, { createContext, useContext, useMemo } from 'react' import { MultisigInstruction } from 'xc_admin_common' -import { useMultisig } from '../hooks/useMultisig' +import { useMultisig, MultisigHookData } from '../hooks/useMultisig' -// TODO: fix any -interface MultisigContextProps { - isLoading: boolean - error: any // TODO: fix any - proposeSquads: SquadsMesh | undefined - voteSquads: SquadsMesh | undefined - upgradeMultisigAccount: MultisigAccount | undefined - priceFeedMultisigAccount: MultisigAccount | undefined - upgradeMultisigProposals: TransactionAccount[] - priceFeedMultisigProposals: TransactionAccount[] - allProposalsIxsParsed: MultisigInstruction[][] - setpriceFeedMultisigProposals: any -} - -const MultisigContext = createContext({ +const MultisigContext = createContext({ upgradeMultisigAccount: undefined, priceFeedMultisigAccount: undefined, upgradeMultisigProposals: [], @@ -29,6 +14,8 @@ const MultisigContext = createContext({ 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, ] ) diff --git a/governance/xc_admin/packages/xc_admin_frontend/contexts/PythContext.tsx b/governance/xc_admin/packages/xc_admin_frontend/contexts/PythContext.tsx index d056d5fe..bc1f0d7a 100644 --- a/governance/xc_admin/packages/xc_admin_frontend/contexts/PythContext.tsx +++ b/governance/xc_admin/packages/xc_admin_frontend/contexts/PythContext.tsx @@ -1,13 +1,24 @@ -import React, { createContext, useContext, useMemo } from 'react' +import React, { + createContext, + useContext, + useEffect, + useMemo, + useState, +} from 'react' import usePyth from '../hooks/usePyth' import { RawConfig } from '../hooks/usePyth' // TODO: fix any +type AccountKeyToSymbol = { [key: string]: string } interface PythContextProps { rawConfig: RawConfig dataIsLoading: boolean error: any connection: any + priceAccountKeyToSymbolMapping: AccountKeyToSymbol + productAccountKeyToSymbolMapping: AccountKeyToSymbol + publisherKeyToNameMapping: Record> + multisigSignerKeyToNameMapping: Record } const PythContext = createContext({ @@ -15,20 +26,47 @@ const PythContext = createContext({ 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> + multisigSignerKeyToNameMapping: Record } - export const PythContextProvider: React.FC = ({ children, + publisherKeyToNameMapping, + multisigSignerKeyToNameMapping, }) => { const { isLoading, error, connection, rawConfig } = usePyth() + const [ + productAccountKeyToSymbolMapping, + setProductAccountKeyToSymbolMapping, + ] = useState({}) + const [priceAccountKeyToSymbolMapping, setPriceAccountKeyToSymbolMapping] = + useState({}) + + 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 = ({ dataIsLoading: isLoading, error, connection, + priceAccountKeyToSymbolMapping, + productAccountKeyToSymbolMapping, + publisherKeyToNameMapping, + multisigSignerKeyToNameMapping, }), - [rawConfig, isLoading, error, connection] + [ + rawConfig, + isLoading, + error, + connection, + publisherKeyToNameMapping, + multisigSignerKeyToNameMapping, + ] ) return {children} diff --git a/governance/xc_admin/packages/xc_admin_frontend/hooks/useMultisig.ts b/governance/xc_admin/packages/xc_admin_frontend/hooks/useMultisig.ts index 6fb92990..a2812c87 100644 --- a/governance/xc_admin/packages/xc_admin_frontend/hooks/useMultisig.ts +++ b/governance/xc_admin/packages/xc_admin_frontend/hooks/useMultisig.ts @@ -1,35 +1,20 @@ -import { Wallet } from '@coral-xyz/anchor' import NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet' -import { getPythProgramKeyForCluster } from '@pythnetwork/client' import { useAnchorWallet } from '@solana/wallet-adapter-react' -import { - AccountMeta, - Cluster, - Connection, - Keypair, - PublicKey, -} from '@solana/web3.js' +import { Connection, Keypair, PublicKey } from '@solana/web3.js' import SquadsMesh from '@sqds/mesh' import { MultisigAccount, TransactionAccount } from '@sqds/mesh/lib/types' -import { useContext, useEffect, useState } from 'react' +import { useCallback, useContext, useEffect, useMemo, useState } from 'react' import { - ExecutePostedVaa, - getManyProposalsInstructions, getMultisigCluster, getProposals, - isRemoteCluster, MultisigInstruction, - MultisigParser, PRICE_FEED_MULTISIG, - PythMultisigInstruction, - UnrecognizedProgram, UPGRADE_MULTISIG, - WormholeMultisigInstruction, } from 'xc_admin_common' import { ClusterContext } from '../contexts/ClusterContext' import { pythClusterApiUrls } from '../utils/pythClusterApiUrl' -interface MultisigHookData { +export interface MultisigHookData { isLoading: boolean error: any // TODO: fix any proposeSquads: SquadsMesh | undefined @@ -39,6 +24,8 @@ interface MultisigHookData { upgradeMultisigProposals: TransactionAccount[] priceFeedMultisigProposals: TransactionAccount[] allProposalsIxsParsed: MultisigInstruction[][] + connection?: Connection + refreshData?: () => { fetchData: () => Promise; cancel: () => void } setpriceFeedMultisigProposals: React.Dispatch< React.SetStateAction > @@ -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() - const [voteSquads, setVoteSquads] = useState() - const anchorWallet = useAnchorWallet() + const [squads, setSquads] = useState() const [urlsIndex, setUrlsIndex] = useState(0) useEffect(() => { - setIsLoading(true) setError(null) }, [urlsIndex, cluster]) @@ -84,39 +69,34 @@ export const useMultisig = (wallet: Wallet): MultisigHookData => { setUrlsIndex(0) }, [cluster]) - useEffect(() => { - const urls = pythClusterApiUrls(getMultisigCluster(cluster)) - const connection = new Connection(urls[urlsIndex].rpcUrl, { + const multisigCluster = useMemo(() => getMultisigCluster(cluster), [cluster]) + + const connection = useMemo(() => { + const urls = pythClusterApiUrls(multisigCluster) + return new Connection(urls[urlsIndex].rpcUrl, { commitment: 'confirmed', wsEndpoint: urls[urlsIndex].wsUrl, }) + }, [urlsIndex, multisigCluster]) + + useEffect(() => { if (wallet) { - setProposeSquads( + setSquads( new SquadsMesh({ connection, wallet, }) ) + } else { + setSquads(undefined) } - if (anchorWallet) { - setVoteSquads( - new SquadsMesh({ - connection, - wallet: anchorWallet as Wallet, - }) - ) - } - }, [wallet, urlsIndex, cluster, anchorWallet]) + }, [wallet, urlsIndex, cluster, connection]) - useEffect(() => { + const refreshData = useCallback(() => { let cancelled = false - const urls = pythClusterApiUrls(getMultisigCluster(cluster)) - const connection = new Connection(urls[urlsIndex].rpcUrl, { - commitment: 'confirmed', - wsEndpoint: urls[urlsIndex].wsUrl, - }) - ;(async () => { + const fetchData = async () => { + setIsLoading(true) try { // mock wallet to allow users to view proposals without connecting their wallet const readOnlySquads = new SquadsMesh({ @@ -125,15 +105,13 @@ export const useMultisig = (wallet: Wallet): MultisigHookData => { }) if (cancelled) return setUpgradeMultisigAccount( - await readOnlySquads.getMultisig( - UPGRADE_MULTISIG[getMultisigCluster(cluster)] - ) + await readOnlySquads.getMultisig(UPGRADE_MULTISIG[multisigCluster]) ) try { if (cancelled) return setpriceFeedMultisigAccount( await readOnlySquads.getMultisig( - PRICE_FEED_MULTISIG[getMultisigCluster(cluster)] + PRICE_FEED_MULTISIG[multisigCluster] ) ) } catch (e) { @@ -142,67 +120,18 @@ export const useMultisig = (wallet: Wallet): MultisigHookData => { } if (cancelled) return - setUpgradeMultisigProposals( - await getSortedProposals( - readOnlySquads, - UPGRADE_MULTISIG[getMultisigCluster(cluster)] - ) + const upgradeProposals = await getSortedProposals( + readOnlySquads, + UPGRADE_MULTISIG[multisigCluster] ) + setUpgradeMultisigProposals(upgradeProposals) try { if (cancelled) return const sortedPriceFeedMultisigProposals = await getSortedProposals( readOnlySquads, - PRICE_FEED_MULTISIG[getMultisigCluster(cluster)] + 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, } } diff --git a/governance/xc_admin/packages/xc_admin_frontend/pages/index.tsx b/governance/xc_admin/packages/xc_admin_frontend/pages/index.tsx index 9f557f59..340807d8 100644 --- a/governance/xc_admin/packages/xc_admin_frontend/pages/index.tsx +++ b/governance/xc_admin/packages/xc_admin_frontend/pages/index.tsx @@ -1,8 +1,4 @@ -import { Wallet } from '@coral-xyz/anchor' -import NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet' import { Tab } from '@headlessui/react' -import { useAnchorWallet } from '@solana/wallet-adapter-react' -import { Keypair } from '@solana/web3.js' import * as fs from 'fs' import type { GetServerSideProps, NextPage } from 'next' import { useRouter } from 'next/router' @@ -88,8 +84,6 @@ const Home: NextPage<{ }) => { const [currentTabIndex, setCurrentTabIndex] = useState(0) const tabInfoArray = Object.values(TAB_INFO) - const anchorWallet = useAnchorWallet() - const wallet = anchorWallet as Wallet const router = useRouter() @@ -123,8 +117,11 @@ const Home: NextPage<{ return ( - - + +
{tabInfoArray[currentTabIndex].queryString === - TAB_INFO.General.queryString ? ( + TAB_INFO.General.queryString && ( - ) : tabInfoArray[currentTabIndex].queryString === - TAB_INFO.UpdatePermissions.queryString ? ( - - ) : tabInfoArray[currentTabIndex].queryString === - TAB_INFO.Proposals.queryString ? ( + )} + {tabInfoArray[currentTabIndex].queryString === + TAB_INFO.UpdatePermissions.queryString && } + {tabInfoArray[currentTabIndex].queryString === + TAB_INFO.Proposals.queryString && ( - + - ) : null} + )} diff --git a/governance/xc_admin/packages/xc_admin_frontend/styles/globals.css b/governance/xc_admin/packages/xc_admin_frontend/styles/globals.css index 4911a6e3..b2227ec7 100644 --- a/governance/xc_admin/packages/xc_admin_frontend/styles/globals.css +++ b/governance/xc_admin/packages/xc_admin_frontend/styles/globals.css @@ -278,13 +278,13 @@ } .dialogTitle { - @apply mb-8 text-center font-body text-[32px] leading-[1.1] lg:mb-11 lg:text-[44px] px-10; + @apply mb-8 px-10 text-center font-body text-[32px] leading-[1.1] lg:mb-11 lg:text-[44px]; } .action-btn { - @apply h-[45px] rounded-full bg-pythPurple px-8 font-mono font-semibold uppercase leading-none transition-colors hover:bg-mediumSlateBlue disabled:opacity-70 disabled:hover:bg-pythPurple; + @apply h-[45px] rounded-full bg-pythPurple px-8 font-mono font-semibold uppercase leading-none transition-colors hover:bg-mediumSlateBlue disabled:opacity-70 disabled:hover:bg-pythPurple; } .sub-action-btn { - @apply h-[45px] rounded-full bg-darkGray2 px-8 font-mono font-semibold uppercase leading-none transition-colors hover:bg-darkGray4 disabled:opacity-70 disabled:hover:bg-darkGray2; + @apply h-[45px] rounded-full bg-darkGray2 px-8 font-mono font-semibold uppercase leading-none transition-colors hover:bg-darkGray4 disabled:opacity-70 disabled:hover:bg-darkGray2; }