import { useWallet } from '@solana/wallet-adapter-react'
import {
AccountMeta,
PublicKey,
SystemProgram,
TransactionInstruction,
} from '@solana/web3.js'
import SquadsMesh from '@sqds/mesh'
import { MultisigAccount, TransactionAccount } from '@sqds/mesh/lib/types'
import {
type ReactNode,
Fragment,
useContext,
useEffect,
useState,
} from 'react'
import toast from 'react-hot-toast'
import {
AnchorMultisigInstruction,
ExecutePostedVaa,
MultisigInstruction,
MultisigParser,
PythMultisigInstruction,
WormholeMultisigInstruction,
getManyProposalsInstructions,
getMultisigCluster,
getProgramName,
} from '@pythnetwork/xc-admin-common'
import { ClusterContext } from '../../../contexts/ClusterContext'
import { useMultisigContext } from '../../../contexts/MultisigContext'
import { usePythContext } from '../../../contexts/PythContext'
import { capitalizeFirstLetter } from '../../../utils/capitalizeFirstLetter'
import {
ParsedAccountPubkeyRow,
SignerTag,
WritableTag,
} from '../../InstructionViews/AccountUtils'
import { WormholeInstructionView } from '../../InstructionViews/WormholeInstructionView'
import CopyText from '../../common/CopyText'
import Spinner from '../../common/Spinner'
import Loadbar from '../../loaders/Loadbar'
import { Wallet } from '@coral-xyz/anchor'
import { PythCluster, getPythProgramKeyForCluster } from '@pythnetwork/client'
import { TransactionBuilder, sendTransactions } from '@pythnetwork/solana-utils'
import { getMappingCluster, isPubkey } from '../../InstructionViews/utils'
import { StatusTag } from './StatusTag'
import { getProposalStatus } from './utils'
import VerifiedIcon from '@images/icons/verified.inline.svg'
import VotedIcon from '@images/icons/voted.inline.svg'
import WarningIcon from '@images/icons/warning.inline.svg'
import * as Tooltip from '@radix-ui/react-tooltip'
import { InstructionsSummary } from './InstructionsSummary'
const IconWithTooltip = ({
icon,
tooltipText,
}: {
icon: ReactNode
tooltipText: string
}) => {
return (
{icon}
{tooltipText}
)
}
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 (
}
tooltipText="You have voted on this proposal."
/>
)
}
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()]})`}
))}
)
}
export const Proposal = ({
proposal,
multisig,
}: {
proposal?: TransactionAccount
multisig?: MultisigAccount
}) => {
const [instructions, setInstructions] = useState([])
const [isTransactionLoading, setIsTransactionLoading] = useState(false)
const { cluster: contextCluster } = useContext(ClusterContext)
const multisigCluster = getMultisigCluster(contextCluster)
const targetClusters: (PythCluster | 'unknown')[] = []
instructions.map((ix) => {
if (!(ix instanceof WormholeMultisigInstruction)) {
targetClusters.push(multisigCluster)
} else if (
ix instanceof WormholeMultisigInstruction &&
ix.governanceAction instanceof ExecutePostedVaa
) {
ix.governanceAction.instructions.map((ix) => {
const remoteClusters: PythCluster[] = [
'pythnet',
'pythtest-conformance',
'pythtest-crosschain',
]
for (const remoteCluster of remoteClusters) {
if (
multisigCluster === getMultisigCluster(remoteCluster) &&
(ix.programId.equals(getPythProgramKeyForCluster(remoteCluster)) ||
ix.programId.equals(SystemProgram.programId))
) {
targetClusters.push(remoteCluster)
}
}
})
} else {
targetClusters.push('unknown')
}
})
const uniqueTargetCluster = new Set(targetClusters).size === 1
const cluster =
uniqueTargetCluster && targetClusters[0] !== 'unknown'
? targetClusters[0]
: contextCluster
const {
walletSquads: squads,
isLoading: isMultisigLoading,
refreshData,
readOnlySquads,
} = useMultisigContext()
const {
priceAccountKeyToSymbolMapping,
productAccountKeyToSymbolMapping,
publisherKeyToNameMapping,
} = usePythContext()
const publisherKeyToNameMappingCluster =
publisherKeyToNameMapping[getMappingCluster(cluster)]
const { publicKey: signerPublicKey } = useWallet()
const proposalStatus = getProposalStatus(proposal, multisig)
const verified =
proposal &&
Object.keys(proposal.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 AnchorMultisigInstruction
)
}) &&
ix.governanceAction.targetChainId === 'pythnet')
)
const voted =
proposal &&
signerPublicKey &&
(proposal.approved.some(
(p) => p.toBase58() === signerPublicKey.toBase58()
) ||
proposal.cancelled.some(
(p) => p.toBase58() === signerPublicKey.toBase58()
) ||
proposal.rejected.some(
(p) => p.toBase58() === signerPublicKey.toBase58()
))
useEffect(() => {
let isCancelled = false
const fetchInstructions = async () => {
if (proposal) {
const proposalInstructions = (
await getManyProposalsInstructions(readOnlySquads, [proposal])
)[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[],
})
)
if (!isCancelled) setInstructions(parsedInstructions)
} else {
if (!isCancelled) setInstructions([])
}
}
fetchInstructions().catch(console.error)
return () => {
isCancelled = true
}
}, [cluster, proposal, readOnlySquads])
const handleClick = async (
instructionGenerator: (
squad: SquadsMesh,
vaultKey: PublicKey,
proposalKey: PublicKey
) => Promise,
msg: string
) => {
if (proposal && squads) {
try {
setIsTransactionLoading(true)
const instruction = await instructionGenerator(
squads,
proposal.ms,
proposal.publicKey
)
const builder = new TransactionBuilder(
squads.wallet.publicKey,
squads.connection
)
builder.addInstruction({
instruction,
signers: [],
computeUnits: 20000,
})
const transactions = builder.buildLegacyTransactions({
computeUnitPriceMicroLamports: 150000,
tightComputeBudget: true,
})
await sendTransactions(
transactions,
squads.connection,
squads.wallet as Wallet
)
if (refreshData) await refreshData().fetchData()
toast.success(msg)
} catch (e: any) {
toast.error(capitalizeFirstLetter(e.message))
} finally {
setIsTransactionLoading(false)
}
}
}
const handleClickApprove = async () => {
await handleClick(
async (
squad: SquadsMesh,
vaultKey: PublicKey,
proposalKey: PublicKey
): Promise => {
return await squad.buildApproveTransaction(vaultKey, proposalKey)
},
`Approved proposal ${proposal?.publicKey.toBase58()}`
)
}
const handleClickReject = async () => {
await handleClick(
async (
squad: SquadsMesh,
vaultKey: PublicKey,
proposalKey: PublicKey
): Promise => {
return await squad.buildRejectTransaction(vaultKey, proposalKey)
},
`Rejected proposal ${proposal?.publicKey.toBase58()}`
)
}
const handleClickExecute = async () => {
await handleClick(
async (
squad: SquadsMesh,
vaultKey: PublicKey,
proposalKey: PublicKey
): Promise => {
return await squad.buildExecuteTransaction(proposalKey)
},
`Executed proposal ${proposal?.publicKey.toBase58()}`
)
}
const handleClickCancel = async () => {
await handleClick(
async (
squad: SquadsMesh,
vaultKey: PublicKey,
proposalKey: PublicKey
): Promise => {
return await squad.buildCancelTransaction(vaultKey, proposalKey)
},
`Cancelled proposal ${proposal?.publicKey.toBase58()}`
)
}
if (!proposal || !multisig || isMultisigLoading)
return (
)
return (
Multisig network: {multisigCluster}
{uniqueTargetCluster
? `Target network: ${targetClusters[0]}`
: targetClusters.length == 0
? ''
: `Multiple target networks detected ${targetClusters.join(' ')}`}
Info
{verified ? (
) : (
)}
{voted && }
Results
Confirmed
{proposal.approved.length}
{proposalStatus === 'active' || proposalStatus === 'rejected' ? (
Rejected
{proposal.rejected.length}
) : (
Cancelled
{proposal.cancelled.length}
)}
Threshold
{multisig.threshold}/{multisig.keys.length}
{proposalStatus === 'active' ? (
) : proposalStatus === 'executeReady' ? (
) : null}
{proposal.approved.length > 0 && (
)}
{proposal.rejected.length > 0 && (
)}
{proposal.cancelled.length > 0 && (
)}
Total Instructions: {instructions.length}
Summary
{instructions?.map((instruction, index) => (
Instruction {index + 1}
Program
{getProgramName(instruction.program)}
{
Instruction Name
{instruction.name}
}
{instruction instanceof WormholeMultisigInstruction &&
instruction.governanceAction ? (
<>
Target Chain
{instruction.governanceAction.targetChainId}
>
) : null}
{instruction instanceof WormholeMultisigInstruction ? null : (
Arguments
{Object.keys(instruction.args).length > 0 ? (
{Object.keys(instruction.args).map((key, index) => (
{key}
{instruction.args[key] instanceof PublicKey ? (
) : typeof instruction.args[key] === 'string' &&
isPubkey(instruction.args[key]) ? (
) : (
{typeof instruction.args[key] === 'string'
? instruction.args[key]
: instruction.args[key] instanceof Uint8Array
? instruction.args[key].toString()
: typeof instruction.args[key] === 'bigint'
? instruction.args[key].toString()
: JSON.stringify(instruction.args[key])}
)}
{key === 'pub' &&
instruction.args[key].toBase58() in
publisherKeyToNameMappingCluster ? (
) : null}
))}
) : (
No arguments
)}
)}
{instruction instanceof WormholeMultisigInstruction && (
)}
{!(instruction instanceof WormholeMultisigInstruction) ? (
Accounts
{Object.keys(instruction.accounts.named).length > 0 ? (
{Object.keys(instruction.accounts.named).map(
(key, index) => (
<>
{key}
{instruction.accounts.named[key].isSigner ? (
) : null}
{instruction.accounts.named[key].isWritable ? (
) : null}
{key === 'priceAccount' &&
instruction.accounts.named[key].pubkey.toBase58() in
priceAccountKeyToSymbolMapping ? (
) : key === 'productAccount' &&
instruction.accounts.named[key].pubkey.toBase58() in
productAccountKeyToSymbolMapping ? (
) : null}
>
)
)}
) : (
No arguments
)}
) : null}
{index !== instructions.length - 1 ? (
) : null}
))}
)
}