[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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 { useAnchorWallet } from '@solana/wallet-adapter-react'
|
||||
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 { useMultisig } from '../hooks/useMultisig'
|
||||
|
||||
|
@ -10,11 +10,17 @@ interface MultisigContextProps {
|
|||
isLoading: boolean
|
||||
error: any // TODO: fix any
|
||||
squads: SquadsMesh | undefined
|
||||
proposals: TransactionAccount[]
|
||||
upgradeMultisigAccount: MultisigAccount | undefined
|
||||
securityMultisigAccount: MultisigAccount | undefined
|
||||
upgradeMultisigProposals: TransactionAccount[]
|
||||
securityMultisigProposals: TransactionAccount[]
|
||||
}
|
||||
|
||||
const MultisigContext = createContext<MultisigContextProps>({
|
||||
proposals: [],
|
||||
upgradeMultisigAccount: undefined,
|
||||
securityMultisigAccount: undefined,
|
||||
upgradeMultisigProposals: [],
|
||||
securityMultisigProposals: [],
|
||||
isLoading: true,
|
||||
error: null,
|
||||
squads: undefined,
|
||||
|
@ -30,18 +36,35 @@ export const MultisigContextProvider: React.FC<
|
|||
MultisigContextProviderProps
|
||||
> = ({ children }) => {
|
||||
const anchorWallet = useAnchorWallet()
|
||||
const { isLoading, error, squads, proposals } = useMultisig(
|
||||
anchorWallet as Wallet
|
||||
)
|
||||
const {
|
||||
isLoading,
|
||||
error,
|
||||
squads,
|
||||
upgradeMultisigAccount,
|
||||
securityMultisigAccount,
|
||||
upgradeMultisigProposals,
|
||||
securityMultisigProposals,
|
||||
} = useMultisig(anchorWallet as Wallet)
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
proposals,
|
||||
upgradeMultisigAccount,
|
||||
securityMultisigAccount,
|
||||
upgradeMultisigProposals,
|
||||
securityMultisigProposals,
|
||||
isLoading,
|
||||
error,
|
||||
squads,
|
||||
}),
|
||||
[squads, isLoading, error, proposals]
|
||||
[
|
||||
squads,
|
||||
isLoading,
|
||||
error,
|
||||
upgradeMultisigAccount,
|
||||
securityMultisigAccount,
|
||||
upgradeMultisigProposals,
|
||||
securityMultisigProposals,
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
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 { TransactionAccount } from '@sqds/mesh/lib/types'
|
||||
import { MultisigAccount, TransactionAccount } from '@sqds/mesh/lib/types'
|
||||
import { useContext, useEffect, useRef, useState } from 'react'
|
||||
import { getMultisigCluster, getProposals } from 'xc_admin_common'
|
||||
import { ClusterContext } from '../contexts/ClusterContext'
|
||||
|
@ -25,7 +25,18 @@ interface MultisigHookData {
|
|||
isLoading: boolean
|
||||
error: any // TODO: fix any
|
||||
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 => {
|
||||
|
@ -33,7 +44,16 @@ export const useMultisig = (wallet: Wallet): MultisigHookData => {
|
|||
const { cluster } = useContext(ClusterContext)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
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 [urlsIndex, setUrlsIndex] = useState(0)
|
||||
|
||||
|
@ -52,21 +72,63 @@ export const useMultisig = (wallet: Wallet): MultisigHookData => {
|
|||
|
||||
connectionRef.current = connection
|
||||
;(async () => {
|
||||
if (wallet) {
|
||||
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,
|
||||
wallet,
|
||||
})
|
||||
setProposals(
|
||||
await getProposals(
|
||||
: new SquadsMesh({
|
||||
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,
|
||||
UPGRADE_MULTISIG[getMultisigCluster(cluster)]
|
||||
)
|
||||
)
|
||||
if (cluster === 'devnet') {
|
||||
setSecurityMultisigProposals(
|
||||
await getSortedProposals(
|
||||
squads,
|
||||
SECURITY_MULTISIG[getMultisigCluster(cluster)]
|
||||
)
|
||||
)
|
||||
} else {
|
||||
setSecurityMultisigProposals([])
|
||||
}
|
||||
setSquads(squads)
|
||||
setIsLoading(false)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
if (cancelled) return
|
||||
if (urlsIndex === urls.length - 1) {
|
||||
// @ts-ignore
|
||||
|
@ -76,13 +138,10 @@ export const useMultisig = (wallet: Wallet): MultisigHookData => {
|
|||
} else if (urlsIndex < urls.length - 1) {
|
||||
setUrlsIndex((urlsIndex) => urlsIndex + 1)
|
||||
console.warn(
|
||||
`Failed with ${urls[urlsIndex]}, trying with ${
|
||||
urls[urlsIndex + 1]
|
||||
}`
|
||||
`Failed with ${urls[urlsIndex]}, trying with ${urls[urlsIndex + 1]}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
return () => {}
|
||||
|
@ -92,6 +151,9 @@ export const useMultisig = (wallet: Wallet): MultisigHookData => {
|
|||
isLoading,
|
||||
error,
|
||||
squads,
|
||||
proposals,
|
||||
upgradeMultisigAccount,
|
||||
securityMultisigAccount,
|
||||
upgradeMultisigProposals,
|
||||
securityMultisigProposals,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,8 +6,10 @@ import Layout from '../components/layout/Layout'
|
|||
import AddRemovePublishers from '../components/tabs/AddRemovePublishers'
|
||||
import General from '../components/tabs/General'
|
||||
import MinPublishers from '../components/tabs/MinPublishers'
|
||||
import Proposals from '../components/tabs/Proposals'
|
||||
import UpdatePermissions from '../components/tabs/UpdatePermissions'
|
||||
import UpdateProductMetadata from '../components/tabs/UpdateProductMetadata'
|
||||
import { MultisigContextProvider } from '../contexts/MultisigContext'
|
||||
import { PythContextProvider } from '../contexts/PythContext'
|
||||
import { classNames } from '../utils/classNames'
|
||||
|
||||
|
@ -38,6 +40,11 @@ const TAB_INFO = {
|
|||
description: 'Update the metadata of a product.',
|
||||
queryString: 'update-product-metadata',
|
||||
},
|
||||
Proposals: {
|
||||
title: 'Proposals',
|
||||
description: 'View and vote on proposals.',
|
||||
queryString: 'proposals',
|
||||
},
|
||||
}
|
||||
|
||||
const DEFAULT_TAB = 'general'
|
||||
|
@ -50,6 +57,9 @@ const Home: NextPage = () => {
|
|||
|
||||
// set current tab value when tab is clicked
|
||||
const handleChangeTab = (index: number) => {
|
||||
if (tabInfoArray[index].queryString !== 'proposals') {
|
||||
delete router.query.proposal
|
||||
}
|
||||
router.query.tab = tabInfoArray[index].queryString
|
||||
setCurrentTabIndex(index)
|
||||
router.push(
|
||||
|
@ -76,6 +86,7 @@ const Home: NextPage = () => {
|
|||
return (
|
||||
<Layout>
|
||||
<PythContextProvider>
|
||||
<MultisigContextProvider>
|
||||
<div className="container relative pt-16 md:pt-20">
|
||||
<div className="py-8 md:py-16">
|
||||
<Tab.Group
|
||||
|
@ -115,7 +126,11 @@ const Home: NextPage = () => {
|
|||
) : tabInfoArray[currentTabIndex].queryString ===
|
||||
TAB_INFO.UpdateProductMetadata.queryString ? (
|
||||
<UpdateProductMetadata />
|
||||
) : tabInfoArray[currentTabIndex].queryString ===
|
||||
TAB_INFO.Proposals.queryString ? (
|
||||
<Proposals />
|
||||
) : null}
|
||||
</MultisigContextProvider>
|
||||
</PythContextProvider>
|
||||
</Layout>
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue