[xc-admin] add proposals page (#570)
* add proposals page * address feedback * show program id for unknown ix * unwrap wormhole ixs * fix error * address feedback * address comments
This commit is contained in:
parent
138fbd924f
commit
a478d9f8cc
|
@ -21,3 +21,19 @@ export function getMultisigCluster(cluster: PythCluster): Cluster | "localnet" {
|
||||||
return cluster;
|
return cluster;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For cluster that are governed remotely (ex : Pythnet from Mainnet) return the network of the remote cluster
|
||||||
|
*/
|
||||||
|
export function getRemoteCluster(
|
||||||
|
cluster: PythCluster
|
||||||
|
): PythCluster | "localnet" {
|
||||||
|
switch (cluster) {
|
||||||
|
case "devnet":
|
||||||
|
return "pythtest";
|
||||||
|
case "mainnet-beta":
|
||||||
|
return "pythnet";
|
||||||
|
default:
|
||||||
|
return cluster;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,780 @@
|
||||||
|
import { BN } from '@coral-xyz/anchor'
|
||||||
|
import { useWallet } from '@solana/wallet-adapter-react'
|
||||||
|
import { AccountMeta, PublicKey } from '@solana/web3.js'
|
||||||
|
import { getIxPDA } from '@sqds/mesh'
|
||||||
|
import { MultisigAccount, TransactionAccount } from '@sqds/mesh/lib/types'
|
||||||
|
import copy from 'copy-to-clipboard'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import {
|
||||||
|
Dispatch,
|
||||||
|
SetStateAction,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
|
import {
|
||||||
|
ExecutePostedVaa,
|
||||||
|
getMultisigCluster,
|
||||||
|
getRemoteCluster,
|
||||||
|
MultisigInstruction,
|
||||||
|
MultisigParser,
|
||||||
|
PythMultisigInstruction,
|
||||||
|
UnrecognizedProgram,
|
||||||
|
WormholeMultisigInstruction,
|
||||||
|
} from 'xc_admin_common'
|
||||||
|
import { ClusterContext } from '../../contexts/ClusterContext'
|
||||||
|
import { useMultisigContext } from '../../contexts/MultisigContext'
|
||||||
|
import CopyIcon from '../../images/icons/copy.inline.svg'
|
||||||
|
import ClusterSwitch from '../ClusterSwitch'
|
||||||
|
import Loadbar from '../loaders/Loadbar'
|
||||||
|
|
||||||
|
const ProposalRow = ({
|
||||||
|
proposal,
|
||||||
|
setCurrentProposalPubkey,
|
||||||
|
}: {
|
||||||
|
proposal: TransactionAccount
|
||||||
|
setCurrentProposalPubkey: Dispatch<SetStateAction<string | undefined>>
|
||||||
|
}) => {
|
||||||
|
const status = Object.keys(proposal.status)[0]
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const handleClickIndividualProposal = useCallback(
|
||||||
|
(proposalPubkey: string) => {
|
||||||
|
router.query.proposal = proposalPubkey
|
||||||
|
setCurrentProposalPubkey(proposalPubkey)
|
||||||
|
router.push(
|
||||||
|
{
|
||||||
|
pathname: router.pathname,
|
||||||
|
query: router.query,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{ scroll: false }
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[setCurrentProposalPubkey, router]
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="my-2 max-h-[58px] cursor-pointer bg-[#1E1B2F] hover:bg-darkGray2"
|
||||||
|
onClick={() =>
|
||||||
|
handleClickIndividualProposal(proposal.publicKey.toBase58())
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between p-4">
|
||||||
|
<div>{proposal.publicKey.toBase58()}</div>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
status === 'active'
|
||||||
|
? 'text-[#E6DAFE]'
|
||||||
|
: status === 'executed'
|
||||||
|
? 'text-[#1FC3D7]'
|
||||||
|
: status === 'cancelled'
|
||||||
|
? 'text-[#FFA7A0]'
|
||||||
|
: status === 'rejected'
|
||||||
|
? 'text-[#F86B86]'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<strong>{status}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SignerTag = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center rounded-full bg-darkGray4 py-1 px-2 text-xs">
|
||||||
|
Signer
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const WritableTag = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center rounded-full bg-offPurple py-1 px-2 text-xs">
|
||||||
|
Writable
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Proposal = ({
|
||||||
|
proposal,
|
||||||
|
multisig,
|
||||||
|
}: {
|
||||||
|
proposal: TransactionAccount | undefined
|
||||||
|
multisig: MultisigAccount | undefined
|
||||||
|
}) => {
|
||||||
|
const [proposalInstructions, setProposalInstructions] = useState<
|
||||||
|
MultisigInstruction[]
|
||||||
|
>([])
|
||||||
|
const [isProposalInstructionsLoading, setIsProposalInstructionsLoading] =
|
||||||
|
useState(false)
|
||||||
|
const { cluster } = useContext(ClusterContext)
|
||||||
|
const { squads, isLoading: isMultisigLoading } = useMultisigContext()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchProposalInstructions = async () => {
|
||||||
|
const multisigParser = MultisigParser.fromCluster(
|
||||||
|
getMultisigCluster(cluster)
|
||||||
|
)
|
||||||
|
if (squads && proposal) {
|
||||||
|
setIsProposalInstructionsLoading(true)
|
||||||
|
const proposalIxs = []
|
||||||
|
for (let i = 1; i <= proposal.instructionIndex; i++) {
|
||||||
|
const instructionPda = getIxPDA(
|
||||||
|
proposal.publicKey,
|
||||||
|
new BN(i),
|
||||||
|
squads.multisigProgramId
|
||||||
|
)[0]
|
||||||
|
const instruction = await squads.getInstruction(instructionPda)
|
||||||
|
const parsedInstruction = multisigParser.parseInstruction({
|
||||||
|
programId: instruction.programId,
|
||||||
|
data: instruction.data as Buffer,
|
||||||
|
keys: instruction.keys as AccountMeta[],
|
||||||
|
})
|
||||||
|
proposalIxs.push(parsedInstruction)
|
||||||
|
}
|
||||||
|
setProposalInstructions(proposalIxs)
|
||||||
|
setIsProposalInstructionsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchProposalInstructions()
|
||||||
|
}, [proposal, squads, cluster])
|
||||||
|
|
||||||
|
return proposal !== undefined &&
|
||||||
|
multisig !== undefined &&
|
||||||
|
!isMultisigLoading &&
|
||||||
|
!isProposalInstructionsLoading ? (
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div className="col-span-3 my-2 space-y-4 bg-[#1E1B2F] p-4 lg:col-span-2">
|
||||||
|
<h4 className="h4 font-semibold">Info</h4>
|
||||||
|
<hr className="border-gray-700" />
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div>Proposal</div>
|
||||||
|
<div>{proposal.publicKey.toBase58()}</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div>Creator</div>
|
||||||
|
<div>{proposal.creator.toBase58()}</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div>Multisig</div>
|
||||||
|
<div>{proposal.ms.toBase58()}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-3 my-2 space-y-4 bg-[#1E1B2F] p-4 lg:col-span-1">
|
||||||
|
<h4 className="h4 mb-4 font-semibold">Results</h4>
|
||||||
|
<hr className="border-gray-700" />
|
||||||
|
<div className="grid grid-cols-3 justify-center gap-4 pt-5 text-center align-middle">
|
||||||
|
<div>
|
||||||
|
<div className="font-bold">Confirmed</div>
|
||||||
|
<div className="text-lg">{proposal.approved.length}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-bold">Cancelled</div>
|
||||||
|
<div className="text-lg">{proposal.cancelled.length}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-bold">Threshold</div>
|
||||||
|
<div className="text-lg">
|
||||||
|
{multisig.threshold}/{multisig.keys.length}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-3 my-2 space-y-4 bg-[#1E1B2F] p-4">
|
||||||
|
<h4 className="h4 font-semibold">Instructions</h4>
|
||||||
|
<hr className="border-gray-700" />
|
||||||
|
{proposalInstructions?.map((instruction, index) => (
|
||||||
|
<>
|
||||||
|
<h4 className="h4 text-[20px] font-semibold">
|
||||||
|
Instruction {index + 1}
|
||||||
|
</h4>
|
||||||
|
<div
|
||||||
|
key={`${index}_instructionType`}
|
||||||
|
className="flex justify-between"
|
||||||
|
>
|
||||||
|
<div>Program</div>
|
||||||
|
<div>
|
||||||
|
{instruction instanceof PythMultisigInstruction
|
||||||
|
? 'Pyth Oracle'
|
||||||
|
: instruction instanceof WormholeMultisigInstruction
|
||||||
|
? 'Wormhole'
|
||||||
|
: 'Unknown'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{instruction instanceof PythMultisigInstruction ||
|
||||||
|
instruction instanceof WormholeMultisigInstruction ? (
|
||||||
|
<div
|
||||||
|
key={`${index}_instructionName`}
|
||||||
|
className="flex justify-between"
|
||||||
|
>
|
||||||
|
<div>Instruction Name</div>
|
||||||
|
<div>{instruction.name}</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{instruction instanceof WormholeMultisigInstruction &&
|
||||||
|
instruction.governanceAction ? (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
key={`${index}_targetChain`}
|
||||||
|
className="flex justify-between"
|
||||||
|
>
|
||||||
|
<div>Target Chain</div>
|
||||||
|
<div>
|
||||||
|
{instruction.governanceAction.targetChainId === 'pythnet' &&
|
||||||
|
cluster === 'devnet'
|
||||||
|
? 'pythtest'
|
||||||
|
: 'pythnet'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
{instruction instanceof WormholeMultisigInstruction ||
|
||||||
|
instruction instanceof UnrecognizedProgram ? null : (
|
||||||
|
<div
|
||||||
|
key={`${index}_arguments`}
|
||||||
|
className="grid grid-cols-4 justify-between"
|
||||||
|
>
|
||||||
|
<div>Arguments</div>
|
||||||
|
{instruction instanceof PythMultisigInstruction ? (
|
||||||
|
Object.keys(instruction.args).length > 0 ? (
|
||||||
|
<div className="col-span-4 mt-2 bg-darkGray2 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(instruction.args).map((key, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex justify-between border-t border-beige-300 py-3"
|
||||||
|
>
|
||||||
|
<div>{key}</div>
|
||||||
|
<div className="max-w-sm break-all">
|
||||||
|
{instruction.args[key] instanceof PublicKey
|
||||||
|
? instruction.args[key].toBase58()
|
||||||
|
: typeof instruction.args[key] === 'string'
|
||||||
|
? instruction.args[key]
|
||||||
|
: instruction.args[key] instanceof Uint8Array
|
||||||
|
? instruction.args[key].toString('hex')
|
||||||
|
: JSON.stringify(instruction.args[key])}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="col-span-3 text-right">No arguments</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<div className="col-span-3 text-right">Unknown</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{instruction instanceof PythMultisigInstruction ? (
|
||||||
|
<div
|
||||||
|
key={`${index}_accounts`}
|
||||||
|
className="grid grid-cols-4 justify-between"
|
||||||
|
>
|
||||||
|
<div>Accounts</div>
|
||||||
|
{Object.keys(instruction.accounts.named).length > 0 ? (
|
||||||
|
<div className="col-span-4 mt-2 bg-darkGray2 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(instruction.accounts.named).map(
|
||||||
|
(key, index) => (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex justify-between border-t border-beige-300 py-3"
|
||||||
|
>
|
||||||
|
<div>{key}</div>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
{instruction.accounts.named[key].isSigner ? (
|
||||||
|
<SignerTag />
|
||||||
|
) : null}
|
||||||
|
{instruction.accounts.named[key].isWritable ? (
|
||||||
|
<WritableTag />
|
||||||
|
) : null}
|
||||||
|
<div
|
||||||
|
className="-ml-1 inline-flex cursor-pointer items-center px-1 hover:bg-dark hover:text-white active:bg-darkGray3"
|
||||||
|
onClick={() => {
|
||||||
|
copy(
|
||||||
|
instruction.accounts.named[
|
||||||
|
key
|
||||||
|
].pubkey.toBase58()
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="mr-2 hidden xl:block">
|
||||||
|
{instruction.accounts.named[
|
||||||
|
key
|
||||||
|
].pubkey.toBase58()}
|
||||||
|
</span>
|
||||||
|
<span className="mr-2 xl:hidden">
|
||||||
|
{instruction.accounts.named[key].pubkey
|
||||||
|
.toBase58()
|
||||||
|
.slice(0, 6) +
|
||||||
|
'...' +
|
||||||
|
instruction.accounts.named[key].pubkey
|
||||||
|
.toBase58()
|
||||||
|
.slice(-6)}
|
||||||
|
</span>{' '}
|
||||||
|
<CopyIcon className="shrink-0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>No arguments</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : instruction instanceof UnrecognizedProgram ? (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
key={`${index}_programId`}
|
||||||
|
className="flex justify-between"
|
||||||
|
>
|
||||||
|
<div>Program ID</div>
|
||||||
|
<div>{instruction.instruction.programId.toBase58()}</div>
|
||||||
|
</div>
|
||||||
|
<div key={`${index}_data`} className="flex justify-between">
|
||||||
|
<div>Data</div>
|
||||||
|
<div>
|
||||||
|
{instruction.instruction.data.length > 0
|
||||||
|
? instruction.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-darkGray2 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>
|
||||||
|
{instruction.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}
|
||||||
|
<div
|
||||||
|
className="-ml-1 inline-flex cursor-pointer items-center px-1 hover:bg-dark hover:text-white active:bg-darkGray3"
|
||||||
|
onClick={() => {
|
||||||
|
copy(key.pubkey.toBase58())
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="mr-2 hidden xl:block">
|
||||||
|
{key.pubkey.toBase58()}
|
||||||
|
</span>
|
||||||
|
<span className="mr-2 xl:hidden">
|
||||||
|
{key.pubkey.toBase58().slice(0, 6) +
|
||||||
|
'...' +
|
||||||
|
key.pubkey.toBase58().slice(-6)}
|
||||||
|
</span>{' '}
|
||||||
|
<CopyIcon className="shrink-0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
{instruction instanceof WormholeMultisigInstruction ? (
|
||||||
|
<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" />
|
||||||
|
{instruction.governanceAction instanceof ExecutePostedVaa
|
||||||
|
? instruction.governanceAction.instructions.map(
|
||||||
|
(innerInstruction, index) => {
|
||||||
|
const multisigParser = MultisigParser.fromCluster(
|
||||||
|
getRemoteCluster(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'
|
||||||
|
: innerInstruction instanceof
|
||||||
|
WormholeMultisigInstruction
|
||||||
|
? 'Wormhole'
|
||||||
|
: 'Unknown'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
key={`${index}_instructionName`}
|
||||||
|
className="flex justify-between"
|
||||||
|
>
|
||||||
|
<div>Instruction Name</div>
|
||||||
|
<div>
|
||||||
|
{parsedInstruction instanceof
|
||||||
|
PythMultisigInstruction ||
|
||||||
|
parsedInstruction instanceof
|
||||||
|
WormholeMultisigInstruction
|
||||||
|
? 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 ? (
|
||||||
|
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>
|
||||||
|
<div className="max-w-sm break-all">
|
||||||
|
{parsedInstruction.args[
|
||||||
|
key
|
||||||
|
] instanceof PublicKey
|
||||||
|
? parsedInstruction.args[
|
||||||
|
key
|
||||||
|
].toBase58()
|
||||||
|
: 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>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</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 ? (
|
||||||
|
<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>{key}</div>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
{parsedInstruction.accounts.named[
|
||||||
|
key
|
||||||
|
].isSigner ? (
|
||||||
|
<SignerTag />
|
||||||
|
) : null}
|
||||||
|
{parsedInstruction.accounts.named[
|
||||||
|
key
|
||||||
|
].isWritable ? (
|
||||||
|
<WritableTag />
|
||||||
|
) : null}
|
||||||
|
<div
|
||||||
|
className="-ml-1 inline-flex cursor-pointer items-center px-1 hover:bg-dark hover:text-white active:bg-darkGray3"
|
||||||
|
onClick={() => {
|
||||||
|
copy(
|
||||||
|
parsedInstruction.accounts.named[
|
||||||
|
key
|
||||||
|
].pubkey.toBase58()
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="mr-2 hidden xl:block">
|
||||||
|
{parsedInstruction.accounts.named[
|
||||||
|
key
|
||||||
|
].pubkey.toBase58()}
|
||||||
|
</span>
|
||||||
|
<span className="mr-2 xl:hidden">
|
||||||
|
{parsedInstruction.accounts.named[
|
||||||
|
key
|
||||||
|
].pubkey
|
||||||
|
.toBase58()
|
||||||
|
.slice(0, 6) +
|
||||||
|
'...' +
|
||||||
|
parsedInstruction.accounts.named[
|
||||||
|
key
|
||||||
|
].pubkey
|
||||||
|
.toBase58()
|
||||||
|
.slice(-6)}
|
||||||
|
</span>{' '}
|
||||||
|
<CopyIcon className="shrink-0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>No arguments</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}
|
||||||
|
<div
|
||||||
|
className="-ml-1 inline-flex cursor-pointer items-center px-1 hover:bg-dark hover:text-white active:bg-darkGray3"
|
||||||
|
onClick={() => {
|
||||||
|
copy(key.pubkey.toBase58())
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="mr-2 hidden xl:block">
|
||||||
|
{key.pubkey.toBase58()}
|
||||||
|
</span>
|
||||||
|
<span className="mr-2 xl:hidden">
|
||||||
|
{key.pubkey
|
||||||
|
.toBase58()
|
||||||
|
.slice(0, 6) +
|
||||||
|
'...' +
|
||||||
|
key.pubkey
|
||||||
|
.toBase58()
|
||||||
|
.slice(-6)}
|
||||||
|
</span>{' '}
|
||||||
|
<CopyIcon className="shrink-0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
: ''}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{index !== proposalInstructions.length - 1 ? (
|
||||||
|
<hr className="border-gray-700" />
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="mt-6">
|
||||||
|
<Loadbar theme="light" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Proposals = () => {
|
||||||
|
const router = useRouter()
|
||||||
|
const [currentProposal, setCurrentProposal] = useState<TransactionAccount>()
|
||||||
|
const [currentProposalPubkey, setCurrentProposalPubkey] = useState<string>()
|
||||||
|
const {
|
||||||
|
securityMultisigAccount,
|
||||||
|
securityMultisigProposals,
|
||||||
|
isLoading: isMultisigLoading,
|
||||||
|
} = useMultisigContext()
|
||||||
|
const { connected } = useWallet()
|
||||||
|
|
||||||
|
const handleClickBackToPriceFeeds = () => {
|
||||||
|
delete router.query.proposal
|
||||||
|
router.push(
|
||||||
|
{
|
||||||
|
pathname: router.pathname,
|
||||||
|
query: router.query,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{ scroll: false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (router.query.proposal) {
|
||||||
|
setCurrentProposalPubkey(router.query.proposal as string)
|
||||||
|
}
|
||||||
|
}, [router.query.proposal])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentProposalPubkey) {
|
||||||
|
const currentProposal = securityMultisigProposals.find(
|
||||||
|
(proposal) => proposal.publicKey.toBase58() === currentProposalPubkey
|
||||||
|
)
|
||||||
|
setCurrentProposal(currentProposal)
|
||||||
|
}
|
||||||
|
}, [currentProposalPubkey, securityMultisigProposals])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<div className="container flex flex-col items-center justify-between lg:flex-row">
|
||||||
|
<div className="mb-4 w-full text-left lg:mb-0">
|
||||||
|
<h1 className="h1 mb-4">
|
||||||
|
{router.query.proposal === undefined ? 'Proposals' : 'Proposal'}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="container min-h-[50vh]">
|
||||||
|
{router.query.proposal === undefined ? (
|
||||||
|
<>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div className="mb-4 md:mb-0">
|
||||||
|
<ClusterSwitch />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="relative mt-6">
|
||||||
|
{isMultisigLoading ? (
|
||||||
|
<div className="mt-3">
|
||||||
|
<Loadbar theme="light" />
|
||||||
|
</div>
|
||||||
|
) : securityMultisigProposals.length > 0 ? (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{securityMultisigProposals.map((proposal, idx) => (
|
||||||
|
<ProposalRow
|
||||||
|
key={idx}
|
||||||
|
proposal={proposal}
|
||||||
|
setCurrentProposalPubkey={setCurrentProposalPubkey}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
"No proposals found. If you're a member of the security multisig, you can create a proposal."
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="max-w-fit cursor-pointer bg-darkGray2 p-3 text-xs font-semibold outline-none transition-colors hover:bg-darkGray3 md:text-base"
|
||||||
|
onClick={handleClickBackToPriceFeeds}
|
||||||
|
>
|
||||||
|
← back to proposals
|
||||||
|
</div>
|
||||||
|
<div className="relative mt-6">
|
||||||
|
<Proposal
|
||||||
|
proposal={currentProposal}
|
||||||
|
multisig={securityMultisigAccount}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Proposals
|
|
@ -1,7 +1,7 @@
|
||||||
import { Wallet } from '@coral-xyz/anchor'
|
import { Wallet } from '@coral-xyz/anchor'
|
||||||
import { useAnchorWallet } from '@solana/wallet-adapter-react'
|
import { useAnchorWallet } from '@solana/wallet-adapter-react'
|
||||||
import SquadsMesh from '@sqds/mesh'
|
import SquadsMesh from '@sqds/mesh'
|
||||||
import { 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 { useMultisig } from '../hooks/useMultisig'
|
import { useMultisig } from '../hooks/useMultisig'
|
||||||
|
|
||||||
|
@ -10,11 +10,17 @@ interface MultisigContextProps {
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
error: any // TODO: fix any
|
error: any // TODO: fix any
|
||||||
squads: SquadsMesh | undefined
|
squads: SquadsMesh | undefined
|
||||||
proposals: TransactionAccount[]
|
upgradeMultisigAccount: MultisigAccount | undefined
|
||||||
|
securityMultisigAccount: MultisigAccount | undefined
|
||||||
|
upgradeMultisigProposals: TransactionAccount[]
|
||||||
|
securityMultisigProposals: TransactionAccount[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const MultisigContext = createContext<MultisigContextProps>({
|
const MultisigContext = createContext<MultisigContextProps>({
|
||||||
proposals: [],
|
upgradeMultisigAccount: undefined,
|
||||||
|
securityMultisigAccount: undefined,
|
||||||
|
upgradeMultisigProposals: [],
|
||||||
|
securityMultisigProposals: [],
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
error: null,
|
error: null,
|
||||||
squads: undefined,
|
squads: undefined,
|
||||||
|
@ -30,18 +36,35 @@ export const MultisigContextProvider: React.FC<
|
||||||
MultisigContextProviderProps
|
MultisigContextProviderProps
|
||||||
> = ({ children }) => {
|
> = ({ children }) => {
|
||||||
const anchorWallet = useAnchorWallet()
|
const anchorWallet = useAnchorWallet()
|
||||||
const { isLoading, error, squads, proposals } = useMultisig(
|
const {
|
||||||
anchorWallet as Wallet
|
isLoading,
|
||||||
)
|
error,
|
||||||
|
squads,
|
||||||
|
upgradeMultisigAccount,
|
||||||
|
securityMultisigAccount,
|
||||||
|
upgradeMultisigProposals,
|
||||||
|
securityMultisigProposals,
|
||||||
|
} = useMultisig(anchorWallet as Wallet)
|
||||||
|
|
||||||
const value = useMemo(
|
const value = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
proposals,
|
upgradeMultisigAccount,
|
||||||
|
securityMultisigAccount,
|
||||||
|
upgradeMultisigProposals,
|
||||||
|
securityMultisigProposals,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
squads,
|
squads,
|
||||||
}),
|
}),
|
||||||
[squads, isLoading, error, proposals]
|
[
|
||||||
|
squads,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
upgradeMultisigAccount,
|
||||||
|
securityMultisigAccount,
|
||||||
|
upgradeMultisigProposals,
|
||||||
|
securityMultisigProposals,
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Wallet } from '@coral-xyz/anchor'
|
import { Wallet } from '@coral-xyz/anchor'
|
||||||
import { Cluster, Connection, PublicKey } from '@solana/web3.js'
|
import { Cluster, Connection, PublicKey, Transaction } from '@solana/web3.js'
|
||||||
import SquadsMesh from '@sqds/mesh'
|
import SquadsMesh from '@sqds/mesh'
|
||||||
import { TransactionAccount } from '@sqds/mesh/lib/types'
|
import { MultisigAccount, TransactionAccount } from '@sqds/mesh/lib/types'
|
||||||
import { useContext, useEffect, useRef, useState } from 'react'
|
import { useContext, useEffect, useRef, useState } from 'react'
|
||||||
import { getMultisigCluster, getProposals } from 'xc_admin_common'
|
import { getMultisigCluster, getProposals } from 'xc_admin_common'
|
||||||
import { ClusterContext } from '../contexts/ClusterContext'
|
import { ClusterContext } from '../contexts/ClusterContext'
|
||||||
|
@ -25,7 +25,18 @@ interface MultisigHookData {
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
error: any // TODO: fix any
|
error: any // TODO: fix any
|
||||||
squads: SquadsMesh | undefined
|
squads: SquadsMesh | undefined
|
||||||
proposals: TransactionAccount[]
|
upgradeMultisigAccount: MultisigAccount | undefined
|
||||||
|
securityMultisigAccount: MultisigAccount | undefined
|
||||||
|
upgradeMultisigProposals: TransactionAccount[]
|
||||||
|
securityMultisigProposals: TransactionAccount[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSortedProposals = async (
|
||||||
|
squads: SquadsMesh,
|
||||||
|
vault: PublicKey
|
||||||
|
): Promise<TransactionAccount[]> => {
|
||||||
|
const proposals = await getProposals(squads, vault)
|
||||||
|
return proposals.sort((a, b) => b.transactionIndex - a.transactionIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useMultisig = (wallet: Wallet): MultisigHookData => {
|
export const useMultisig = (wallet: Wallet): MultisigHookData => {
|
||||||
|
@ -33,7 +44,16 @@ export const useMultisig = (wallet: Wallet): MultisigHookData => {
|
||||||
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)
|
||||||
const [proposals, setProposals] = useState<TransactionAccount[]>([])
|
const [upgradeMultisigAccount, setUpgradeMultisigAccount] =
|
||||||
|
useState<MultisigAccount>()
|
||||||
|
const [securityMultisigAccount, setSecurityMultisigAccount] =
|
||||||
|
useState<MultisigAccount>()
|
||||||
|
const [upgradeMultisigProposals, setUpgradeMultisigProposals] = useState<
|
||||||
|
TransactionAccount[]
|
||||||
|
>([])
|
||||||
|
const [securityMultisigProposals, setSecurityMultisigProposals] = useState<
|
||||||
|
TransactionAccount[]
|
||||||
|
>([])
|
||||||
const [squads, setSquads] = useState<SquadsMesh>()
|
const [squads, setSquads] = useState<SquadsMesh>()
|
||||||
const [urlsIndex, setUrlsIndex] = useState(0)
|
const [urlsIndex, setUrlsIndex] = useState(0)
|
||||||
|
|
||||||
|
@ -52,21 +72,63 @@ export const useMultisig = (wallet: Wallet): MultisigHookData => {
|
||||||
|
|
||||||
connectionRef.current = connection
|
connectionRef.current = connection
|
||||||
;(async () => {
|
;(async () => {
|
||||||
if (wallet) {
|
|
||||||
try {
|
try {
|
||||||
const squads = new SquadsMesh({
|
// mock wallet to allow users to view proposals without connecting their wallet
|
||||||
|
const signTransaction = () =>
|
||||||
|
new Promise<Transaction>((resolve) => {
|
||||||
|
resolve(new Transaction())
|
||||||
|
})
|
||||||
|
const signAllTransactions = () =>
|
||||||
|
new Promise<Transaction[]>((resolve) => {
|
||||||
|
resolve([new Transaction()])
|
||||||
|
})
|
||||||
|
const squads = wallet
|
||||||
|
? new SquadsMesh({
|
||||||
connection,
|
connection,
|
||||||
wallet,
|
wallet,
|
||||||
})
|
})
|
||||||
setProposals(
|
: new SquadsMesh({
|
||||||
await getProposals(
|
connection,
|
||||||
|
wallet: {
|
||||||
|
signTransaction: () => signTransaction(),
|
||||||
|
signAllTransactions: () => signAllTransactions(),
|
||||||
|
publicKey: new PublicKey(0),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
setUpgradeMultisigAccount(
|
||||||
|
await squads.getMultisig(
|
||||||
|
UPGRADE_MULTISIG[getMultisigCluster(cluster)]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (cluster === 'devnet') {
|
||||||
|
setSecurityMultisigAccount(
|
||||||
|
await squads.getMultisig(
|
||||||
|
SECURITY_MULTISIG[getMultisigCluster(cluster)]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
setSecurityMultisigAccount(undefined)
|
||||||
|
}
|
||||||
|
setUpgradeMultisigProposals(
|
||||||
|
await getSortedProposals(
|
||||||
squads,
|
squads,
|
||||||
UPGRADE_MULTISIG[getMultisigCluster(cluster)]
|
UPGRADE_MULTISIG[getMultisigCluster(cluster)]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
if (cluster === 'devnet') {
|
||||||
|
setSecurityMultisigProposals(
|
||||||
|
await getSortedProposals(
|
||||||
|
squads,
|
||||||
|
SECURITY_MULTISIG[getMultisigCluster(cluster)]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
setSecurityMultisigProposals([])
|
||||||
|
}
|
||||||
setSquads(squads)
|
setSquads(squads)
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
if (urlsIndex === urls.length - 1) {
|
if (urlsIndex === urls.length - 1) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -76,13 +138,10 @@ export const useMultisig = (wallet: Wallet): MultisigHookData => {
|
||||||
} else if (urlsIndex < urls.length - 1) {
|
} else if (urlsIndex < urls.length - 1) {
|
||||||
setUrlsIndex((urlsIndex) => urlsIndex + 1)
|
setUrlsIndex((urlsIndex) => urlsIndex + 1)
|
||||||
console.warn(
|
console.warn(
|
||||||
`Failed with ${urls[urlsIndex]}, trying with ${
|
`Failed with ${urls[urlsIndex]}, trying with ${urls[urlsIndex + 1]}`
|
||||||
urls[urlsIndex + 1]
|
|
||||||
}`
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})()
|
})()
|
||||||
|
|
||||||
return () => {}
|
return () => {}
|
||||||
|
@ -92,6 +151,9 @@ export const useMultisig = (wallet: Wallet): MultisigHookData => {
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
squads,
|
squads,
|
||||||
proposals,
|
upgradeMultisigAccount,
|
||||||
|
securityMultisigAccount,
|
||||||
|
upgradeMultisigProposals,
|
||||||
|
securityMultisigProposals,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,10 @@ import Layout from '../components/layout/Layout'
|
||||||
import AddRemovePublishers from '../components/tabs/AddRemovePublishers'
|
import AddRemovePublishers from '../components/tabs/AddRemovePublishers'
|
||||||
import General from '../components/tabs/General'
|
import General from '../components/tabs/General'
|
||||||
import MinPublishers from '../components/tabs/MinPublishers'
|
import MinPublishers from '../components/tabs/MinPublishers'
|
||||||
|
import Proposals from '../components/tabs/Proposals'
|
||||||
import UpdatePermissions from '../components/tabs/UpdatePermissions'
|
import UpdatePermissions from '../components/tabs/UpdatePermissions'
|
||||||
import UpdateProductMetadata from '../components/tabs/UpdateProductMetadata'
|
import UpdateProductMetadata from '../components/tabs/UpdateProductMetadata'
|
||||||
|
import { MultisigContextProvider } from '../contexts/MultisigContext'
|
||||||
import { PythContextProvider } from '../contexts/PythContext'
|
import { PythContextProvider } from '../contexts/PythContext'
|
||||||
import { classNames } from '../utils/classNames'
|
import { classNames } from '../utils/classNames'
|
||||||
|
|
||||||
|
@ -38,6 +40,11 @@ const TAB_INFO = {
|
||||||
description: 'Update the metadata of a product.',
|
description: 'Update the metadata of a product.',
|
||||||
queryString: 'update-product-metadata',
|
queryString: 'update-product-metadata',
|
||||||
},
|
},
|
||||||
|
Proposals: {
|
||||||
|
title: 'Proposals',
|
||||||
|
description: 'View and vote on proposals.',
|
||||||
|
queryString: 'proposals',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_TAB = 'general'
|
const DEFAULT_TAB = 'general'
|
||||||
|
@ -50,6 +57,9 @@ const Home: NextPage = () => {
|
||||||
|
|
||||||
// set current tab value when tab is clicked
|
// set current tab value when tab is clicked
|
||||||
const handleChangeTab = (index: number) => {
|
const handleChangeTab = (index: number) => {
|
||||||
|
if (tabInfoArray[index].queryString !== 'proposals') {
|
||||||
|
delete router.query.proposal
|
||||||
|
}
|
||||||
router.query.tab = tabInfoArray[index].queryString
|
router.query.tab = tabInfoArray[index].queryString
|
||||||
setCurrentTabIndex(index)
|
setCurrentTabIndex(index)
|
||||||
router.push(
|
router.push(
|
||||||
|
@ -76,6 +86,7 @@ const Home: NextPage = () => {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<PythContextProvider>
|
<PythContextProvider>
|
||||||
|
<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
|
||||||
|
@ -115,7 +126,11 @@ const Home: NextPage = () => {
|
||||||
) : tabInfoArray[currentTabIndex].queryString ===
|
) : tabInfoArray[currentTabIndex].queryString ===
|
||||||
TAB_INFO.UpdateProductMetadata.queryString ? (
|
TAB_INFO.UpdateProductMetadata.queryString ? (
|
||||||
<UpdateProductMetadata />
|
<UpdateProductMetadata />
|
||||||
|
) : tabInfoArray[currentTabIndex].queryString ===
|
||||||
|
TAB_INFO.Proposals.queryString ? (
|
||||||
|
<Proposals />
|
||||||
) : null}
|
) : null}
|
||||||
|
</MultisigContextProvider>
|
||||||
</PythContextProvider>
|
</PythContextProvider>
|
||||||
</Layout>
|
</Layout>
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue