* useVetoingPop

* show and dis/enable veto button

* refactor to hook

* tc

* refactor action to accept all vote kinds

* refactor: let components handle undefined data

* add vetos to useProposalVotes

* .

* version proposalVotes results

* refactor ApprovalQuorum

* generalize quorum component

* rename file

* exh-deps

* [unrelated] always show multi vote ui

* show veto progress

* kill unused UI

* rename file to match component

* add assertUnreachable for exhaustive switches

* vetoed proposal badge

* show veto progress on card iff there are any vetos

* ProposalSelectCard always shows veto

* always show multi vote

* consolidate some duplicate code

* search for exhaustive logic involving proposal state and add Vetoed

* proposal result shows veto

* [wip] refactor vote panel components, break them up

* .

* break up files

* add vetoed filter

* fix imports

* rename files

* refine logic

* more refactor

* refactor

* correct some max vote weight logic

* lint

* comments

* exhaustive YouVoted display

* use react query to efficiently grab vote records

* .

* refactor our old ownVoteRecord hook

* show both veto and electoral vote status

* incorporate new voterecord hooks

* handle not-found accounts

* add react query devtools

* fix

* fix typescript

* murder husky

* delete accidental paren

* clearer language

* invalidate voteRecord upon voting

* Fix oversight for checking if a token is the council mint while doing some maths

* remove debug console logs

* exclude vetos from vote explorer
This commit is contained in:
agrippa 2022-12-13 12:36:19 -05:00 committed by GitHub
parent ded1393950
commit a1a33b92a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1244 additions and 638 deletions

View File

@ -1,4 +1,2 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn lint-staged

View File

@ -6,8 +6,9 @@ import {
Proposal,
Realm,
TokenOwnerRecord,
VoteChoice,
VoteKind,
withPostChatMessage,
YesNoVote,
} from '@solana/spl-governance'
import { ProgramAccount } from '@solana/spl-governance'
import { RpcContext } from '@solana/spl-governance'
@ -27,12 +28,27 @@ import { NftVoterClient } from '@solana/governance-program-library'
import { calcCostOfNftVote, checkHasEnoughSolToVote } from '@tools/nftVoteCalc'
import useNftProposalStore from 'NftVotePlugin/NftProposalStore'
const getVetoTokenMint = (
proposal: ProgramAccount<Proposal>,
realm: ProgramAccount<Realm>
) => {
const communityMint = realm.account.communityMint
const councilMint = realm.account.config.councilMint
const governingMint = proposal.account.governingTokenMint
const vetoTokenMint = governingMint.equals(communityMint)
? councilMint
: communityMint
if (vetoTokenMint === undefined)
throw new Error('There is no token that can veto this proposal')
return vetoTokenMint
}
export async function castVote(
{ connection, wallet, programId, walletPubkey }: RpcContext,
realm: ProgramAccount<Realm>,
proposal: ProgramAccount<Proposal>,
tokeOwnerRecord: ProgramAccount<TokenOwnerRecord>,
yesNoVote: YesNoVote,
voteKind: VoteKind,
message?: ChatMessageBody | undefined,
votingPlugin?: VotingClient,
runAfterConfirmation?: (() => void) | null
@ -57,6 +73,42 @@ export async function castVote(
tokeOwnerRecord
)
// It is not clear that defining these extraneous fields, `deny` and `veto`, is actually necessary.
// See: https://discord.com/channels/910194960941338677/910630743510777926/1044741454175674378
const vote =
voteKind === VoteKind.Approve
? new Vote({
voteType: VoteKind.Approve,
approveChoices: [new VoteChoice({ rank: 0, weightPercentage: 100 })],
deny: undefined,
veto: undefined,
})
: voteKind === VoteKind.Deny
? new Vote({
voteType: VoteKind.Deny,
approveChoices: undefined,
deny: true,
veto: undefined,
})
: voteKind == VoteKind.Veto
? new Vote({
voteType: VoteKind.Veto,
veto: true,
deny: undefined,
approveChoices: undefined,
})
: new Vote({
voteType: VoteKind.Abstain,
veto: undefined,
deny: undefined,
approveChoices: undefined,
})
const tokenMint =
voteKind === VoteKind.Veto
? getVetoTokenMint(proposal, realm)
: proposal.account.governingTokenMint
await withCastVote(
instructions,
programId,
@ -67,8 +119,8 @@ export async function castVote(
proposal.account.tokenOwnerRecord,
tokeOwnerRecord.pubkey,
governanceAuthority,
proposal.account.governingTokenMint,
Vote.fromYesNoVote(yesNoVote),
tokenMint,
vote,
payer,
plugin?.voterWeightPk,
plugin?.maxVoterWeightRecord

View File

@ -1,64 +0,0 @@
import Tooltip from './Tooltip'
import {
CheckCircleIcon,
InformationCircleIcon,
} from '@heroicons/react/outline'
type ApprovalProgressProps = {
progress: number
showBg?: boolean
yesVotesRequired: number
}
const ApprovalProgress = ({
progress,
showBg,
yesVotesRequired,
}: ApprovalProgressProps) => {
return (
<div className={`${showBg ? 'bg-bkg-1 p-3' : ''} rounded-md`}>
<div className="flex items-center">
<div className="w-full">
<div className="flex items-center">
<p className="text-fgd-2 mb-0 mr-1.5">Approval Quorum</p>
<Tooltip content="Proposals must reach a minimum number of 'Yes' votes before they are eligible to pass. If the minimum is reached but there are more 'No' votes when voting ends the proposal will fail.">
<InformationCircleIcon className="cursor-help h-5 text-fgd-2 w-5" />
</Tooltip>
</div>
{progress < 100 ? (
<p className="font-bold mb-0 text-fgd-1">{`${yesVotesRequired?.toLocaleString(
undefined,
{
maximumFractionDigits: 0,
}
)} ${progress > 0 ? 'more' : ''} Yes vote${
yesVotesRequired > 1 ? 's' : ''
} required`}</p>
) : (
<div className="flex items-center">
<CheckCircleIcon className="flex-shrink-0 h-5 mr-1.5 text-green w-5" />
<p className="font-bold mb-0 text-fgd-1">
Required approval achieved
</p>
</div>
)}
</div>
</div>
{/* {progress < 100 ? ( */}
<div className="bg-bkg-4 h-2 flex flex-grow mt-2.5 rounded w-full">
<div
style={{
width: `${progress}%`,
}}
className={`${
progress >= 100 ? 'bg-green' : 'bg-fgd-3'
} flex rounded`}
></div>
</div>
{/* ) : null} */}
</div>
)
}
export default ApprovalProgress

View File

@ -32,7 +32,7 @@ export const BaseGovernanceFormV3 = ({
// @asktree: unclear that this should not just be an effect in the parent, I am just replicating the behavior of previous components
useEffect(() => {
setFormErrors({})
}, [form])
}, [form, setFormErrors])
const [communityTokenHelpers, councilTokenHelpers] = useMemo(() => {
return [realmMint, councilMint].map((mint) => {

View File

@ -1,9 +1,9 @@
import styled from '@emotion/styled'
import { ChevronRightIcon } from '@heroicons/react/solid'
import ProposalStateBadge from './ProposalStatusBadge'
import ProposalStateBadge from './ProposalStateBadge'
import Link from 'next/link'
import { Proposal, ProposalState } from '@solana/spl-governance'
import ApprovalQuorum from './ApprovalQuorum'
import { ApprovalProgress, VetoProgress } from './QuorumProgress'
import useRealm from '../hooks/useRealm'
import useProposalVotes from '../hooks/useProposalVotes'
import ProposalTimeStatus from './ProposalTimeStatus'
@ -31,7 +31,7 @@ const StyledCardWrapper = styled.div`
const ProposalCard = ({ proposalPk, proposal }: ProposalCardProps) => {
const { symbol } = useRealm()
const { fmtUrlWithCluster } = useQueryContext()
const { yesVoteProgress, yesVotesRequired } = useProposalVotes(proposal)
const votesData = useProposalVotes(proposal)
return (
<div>
@ -61,16 +61,34 @@ const ProposalCard = ({ proposalPk, proposal }: ProposalCardProps) => {
<ProposalTimeStatus proposal={proposal} />
</div>
{proposal.state === ProposalState.Voting && (
<div className="border-t border-fgd-4 flex flex-col lg:flex-row mt-2 p-4">
<div className="pb-3 lg:pb-0 lg:border-r lg:border-fgd-4 lg:pr-4 w-full lg:w-1/2">
<div className="border-t border-fgd-4 flex flex-col lg:flex-row mt-2 p-4 gap-x-4 gap-y-3">
<div className="w-full lg:w-auto flex-1">
<VoteResults isListView proposal={proposal} />
</div>
<div className="lg:pl-4 w-full lg:w-1/2">
<ApprovalQuorum
progress={yesVoteProgress}
yesVotesRequired={yesVotesRequired}
<div className="border-r border-fgd-4 hidden lg:block" />
<div className="w-full lg:w-auto flex-1">
<ApprovalProgress
progress={votesData.yesVoteProgress}
votesRequired={votesData.yesVotesRequired}
/>
</div>
{votesData._programVersion !== undefined &&
// @asktree: here is some typescript gore because typescript doesn't know that a number being > 3 means it isn't 1 or 2
votesData._programVersion !== 1 &&
votesData._programVersion !== 2 &&
votesData.veto !== undefined &&
votesData.veto.voteProgress > 0 ? (
<>
<div className="border-r border-fgd-4 hidden lg:block" />
<div className="w-full lg:w-auto flex-1">
<VetoProgress
progress={votesData.veto.voteProgress}
votesRequired={votesData.veto.votesRequired}
/>
</div>
</>
) : undefined}
</div>
)}
</StyledCardWrapper>

View File

@ -13,6 +13,7 @@ export const InitialFilters = {
ExecutingWithErrors: true,
SigningOff: true,
Voting: true,
Vetoed: true,
}
export type Filters = typeof InitialFilters

View File

@ -1,7 +1,7 @@
import { CheckIcon } from '@heroicons/react/solid'
import ProposalStateBadge from './ProposalStatusBadge'
import ProposalStateBadge from './ProposalStateBadge'
import { Proposal, ProposalState } from '@solana/spl-governance'
import ApprovalQuorum from './ApprovalQuorum'
import { ApprovalProgress, VetoProgress } from './QuorumProgress'
import useProposalVotes from '../hooks/useProposalVotes'
import ProposalTimeStatus from './ProposalTimeStatus'
import { PublicKey } from '@solana/web3.js'
@ -21,7 +21,7 @@ const ProposalSelectCard = ({
setSelectedProposals,
selectedProposals,
}: ProposalCardProps) => {
const { yesVoteProgress, yesVotesRequired } = useProposalVotes(proposal)
const votesData = useProposalVotes(proposal)
const checked = !!selectedProposals.find(
// @ts-ignore
@ -66,16 +66,32 @@ const ProposalSelectCard = ({
<ProposalTimeStatus proposal={proposal} />
</div>
{proposal.state === ProposalState.Voting && (
<div className="border-t border-fgd-4 flex flex-col lg:flex-row mt-2 p-4">
<div className="pb-3 lg:pb-0 lg:border-r lg:border-fgd-4 lg:pr-4 w-full lg:w-1/2">
<div className="border-t border-fgd-4 flex flex-col lg:flex-row mt-2 p-4 gap-x-4 gap-y-3">
<div className="w-full lg:w-auto flex-1">
<VoteResults isListView proposal={proposal} />
</div>
<div className="lg:pl-4 w-full lg:w-1/2">
<ApprovalQuorum
progress={yesVoteProgress}
yesVotesRequired={yesVotesRequired}
<div className="border-r border-fgd-4 hidden lg:block" />
<div className="w-full lg:w-auto flex-1">
<ApprovalProgress
progress={votesData.yesVoteProgress}
votesRequired={votesData.yesVotesRequired}
/>
</div>
{votesData._programVersion !== undefined &&
// @asktree: here is some typescript gore because typescript doesn't know that a number being > 3 means it isn't 1 or 2
votesData._programVersion !== 1 &&
votesData._programVersion !== 2 &&
votesData.veto !== undefined ? (
<>
<div className="border-r border-fgd-4 hidden lg:block" />
<div className="w-full lg:w-auto flex-1">
<VetoProgress
progress={votesData.veto.voteProgress}
votesRequired={votesData.veto.votesRequired}
/>
</div>
</>
) : undefined}
</div>
)}
</button>

View File

@ -3,6 +3,7 @@ import classNames from 'classnames'
import useRealm from '@hooks/useRealm'
import useRealmGovernance from '../hooks/useRealmGovernance'
import assertUnreachable from '@utils/typescript/assertUnreachable'
export const hasInstructions = (proposal: Proposal) => {
if (proposal.instructionsCount) {
@ -33,6 +34,7 @@ function getBorderColor(proposalState: ProposalState, otherState: OtherState) {
case ProposalState.Completed:
case ProposalState.Defeated:
case ProposalState.ExecutingWithErrors:
case ProposalState.Vetoed:
return 'border-transparent'
case ProposalState.Executing:
return 'border-[#5DC9EB]'
@ -46,6 +48,8 @@ function getBorderColor(proposalState: ProposalState, otherState: OtherState) {
: 'border-[#5DC9EB]'
case ProposalState.Voting:
return otherState.votingEnded ? 'border-[#5DC9EB]' : 'border-[#8EFFDD]'
default:
assertUnreachable(proposalState)
}
}
@ -72,6 +76,10 @@ function getLabel(
return !hasInstructions(otherState.proposal) ? 'Completed' : 'Executable'
case ProposalState.Voting:
return otherState.votingEnded ? 'Finalizing' : 'Voting'
case ProposalState.Vetoed:
return 'Vetoed'
default:
assertUnreachable(proposalState)
}
}
@ -84,6 +92,7 @@ function getOpacity(
case ProposalState.Completed:
case ProposalState.Defeated:
case ProposalState.ExecutingWithErrors:
case ProposalState.Vetoed:
return 'opacity-70'
case ProposalState.Draft:
return otherState.isCreator ? '' : 'opacity-70'
@ -91,8 +100,11 @@ function getOpacity(
return otherState.isSignatory ? '' : 'opacity-70'
case ProposalState.Succeeded:
return !hasInstructions(otherState.proposal) ? 'opacity-70' : ''
default:
case ProposalState.Voting:
case ProposalState.Executing:
return ''
default:
assertUnreachable(proposalState)
}
}
@ -107,6 +119,7 @@ function getTextColor(
case ProposalState.Completed:
return 'text-[#8EFFDD]'
case ProposalState.Defeated:
case ProposalState.Vetoed:
case ProposalState.ExecutingWithErrors:
return 'text-[#FF7C7C]'
case ProposalState.Executing:
@ -121,6 +134,8 @@ function getTextColor(
return otherState.votingEnded
? 'bg-gradient-to-r from-[#00C2FF] via-[#00E4FF] to-[#87F2FF] bg-clip-text text-transparent'
: 'text-[#8EFFDD]'
default:
assertUnreachable(proposalState)
}
}

View File

@ -0,0 +1,74 @@
import Tooltip from './Tooltip'
import { InformationCircleIcon } from '@heroicons/react/outline'
type Props = {
progress?: number
votesRequired?: number
showBg?: boolean
}
type GenericProps = {
voteKindTitle: string
quorumTitle: string
tooltip: string
}
const QuorumProgress = ({
progress,
showBg,
votesRequired,
quorumTitle,
tooltip,
voteKindTitle,
}: Props & GenericProps) => {
return (
<div className={`${showBg ? 'bg-bkg-1 p-3' : ''} rounded-md`}>
<div className="flex items-center">
<div className="w-full">
<div className="flex items-center">
<p className="text-fgd-2 mb-0 mr-1.5">{quorumTitle} Quorum</p>
<Tooltip content={tooltip}>
<InformationCircleIcon className="cursor-help h-5 text-fgd-2 w-5" />
</Tooltip>
</div>
<p className="font-bold mb-0 text-fgd-1">{`${(
votesRequired ?? 0
).toLocaleString(undefined, {
maximumFractionDigits: 0,
})} ${(progress ?? 0) > 0 ? 'more' : ''} ${voteKindTitle} vote${
(votesRequired ?? 0) > 1 ? 's' : ''
} required`}</p>
</div>
</div>
{/* {progress < 100 ? ( */}
<div className="bg-bkg-4 h-2 flex flex-grow mt-2.5 rounded w-full">
<div
style={{
width: `${progress}%`,
}}
className={`${
(progress ?? 0) >= 100 ? 'bg-green' : 'bg-fgd-3'
} flex rounded`}
></div>
</div>
{/* ) : null} */}
</div>
)
}
export const ApprovalProgress = (props: Props) => (
<QuorumProgress
tooltip={`Proposals must reach a minimum number of 'Yes' votes before they are eligible to pass. If the minimum is reached but there are more 'No' votes when voting ends the proposal will fail.`}
quorumTitle="Approval"
voteKindTitle="Yes"
{...props}
/>
)
export const VetoProgress = (props: Props) => (
<QuorumProgress
tooltip={`This proposal can be vetoed. If the veto quorum is reached the proposal will fail regardless of the approval quorum.`}
quorumTitle="Veto"
voteKindTitle="Veto"
{...props}
/>
)

View File

@ -1,9 +1,9 @@
import React, { FunctionComponent, useState } from 'react'
import { ThumbDownIcon, ThumbUpIcon } from '@heroicons/react/solid'
import { BanIcon, ThumbDownIcon, ThumbUpIcon } from '@heroicons/react/solid'
import {
ChatMessageBody,
ChatMessageBodyType,
YesNoVote,
VoteKind,
} from '@solana/spl-governance'
import { RpcContext } from '@solana/spl-governance'
import useWalletStore from '../stores/useWalletStore'
@ -23,25 +23,29 @@ import useVotePluginsClientStore from 'stores/useVotePluginsClientStore'
import { nftPluginsPks } from '@hooks/useVotingPlugins'
import useNftProposalStore from 'NftVotePlugin/NftProposalStore'
import { NftVoterClient } from '@solana/governance-program-library'
import queryClient from '@hooks/queries/queryClient'
import { voteRecordQueryKeys } from '@hooks/queries/voteRecord'
interface VoteCommentModalProps {
onClose: () => void
isOpen: boolean
vote: YesNoVote
vote: VoteKind
voterTokenRecord: ProgramAccount<TokenOwnerRecord>
}
const VoteCommentModal: FunctionComponent<VoteCommentModalProps> = ({
const useSubmitVote = ({
comment,
onClose,
isOpen,
vote,
voterTokenRecord,
}: {
comment: string
onClose: () => void
voterTokenRecord: ProgramAccount<TokenOwnerRecord>
}) => {
const client = useVotePluginsClientStore(
(s) => s.state.currentRealmVotingClient
)
const [submitting, setSubmitting] = useState(false)
const [comment, setComment] = useState('')
const wallet = useWalletStore((s) => s.current)
const connection = useWalletStore((s) => s.connection)
const { proposal } = useWalletStore((s) => s.selectedProposal)
@ -54,7 +58,7 @@ const VoteCommentModal: FunctionComponent<VoteCommentModalProps> = ({
config?.account.communityTokenConfig.voterWeightAddin?.toBase58()
)
const { closeNftVotingCountingModal } = useNftProposalStore.getState()
const submitVote = async (vote: YesNoVote) => {
const submitVote = async (vote: VoteKind) => {
setSubmitting(true)
const rpcContext = new RpcContext(
proposal!.owner,
@ -71,6 +75,12 @@ const VoteCommentModal: FunctionComponent<VoteCommentModalProps> = ({
})
: undefined
const confirmationCallback = async () => {
await refetchProposals()
// TODO refine this to only invalidate the one query
await queryClient.invalidateQueries(voteRecordQueryKeys.all)
}
try {
await castVote(
rpcContext,
@ -80,7 +90,7 @@ const VoteCommentModal: FunctionComponent<VoteCommentModalProps> = ({
vote,
msg,
client,
refetchProposals
confirmationCallback
)
if (!isNftPlugin) {
await refetchProposals()
@ -104,7 +114,30 @@ const VoteCommentModal: FunctionComponent<VoteCommentModalProps> = ({
fetchChatMessages(proposal!.pubkey)
}
const voteString = vote === YesNoVote.Yes ? 'Yes' : 'No'
return { submitting, submitVote }
}
const VOTE_STRINGS = {
[VoteKind.Approve]: 'Yes',
[VoteKind.Deny]: 'No',
[VoteKind.Veto]: 'Veto',
[VoteKind.Abstain]: 'Abstain',
}
const VoteCommentModal: FunctionComponent<VoteCommentModalProps> = ({
onClose,
isOpen,
vote,
voterTokenRecord,
}) => {
const [comment, setComment] = useState('')
const { submitting, submitVote } = useSubmitVote({
comment,
onClose,
voterTokenRecord,
})
const voteString = VOTE_STRINGS[vote]
return (
<Modal onClose={onClose} isOpen={isOpen}>
@ -136,10 +169,12 @@ const VoteCommentModal: FunctionComponent<VoteCommentModalProps> = ({
>
<div className="flex items-center">
{!submitting &&
(vote === YesNoVote.Yes ? (
(vote === VoteKind.Approve ? (
<ThumbUpIcon className="h-4 w-4 fill-black mr-2" />
) : (
) : vote === VoteKind.Deny ? (
<ThumbDownIcon className="h-4 w-4 fill-black mr-2" />
) : (
<BanIcon className="h-4 w-4 fill-black mr-2" />
))}
{submitting ? <Loading /> : <span>Vote {voteString}</span>}
</div>

View File

@ -1,311 +0,0 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { withFinalizeVote, YesNoVote } from '@solana/spl-governance'
import { TransactionInstruction } from '@solana/web3.js'
import { useCallback, useState } from 'react'
import { relinquishVote } from '../actions/relinquishVote'
import { useHasVoteTimeExpired } from '../hooks/useHasVoteTimeExpired'
import useRealm from '../hooks/useRealm'
import { ProposalState } from '@solana/spl-governance'
import { RpcContext } from '@solana/spl-governance'
import { GoverningTokenRole } from '@solana/spl-governance'
import { BanIcon, ThumbUpIcon, ThumbDownIcon } from '@heroicons/react/solid'
import useWalletStore from '../stores/useWalletStore'
import Button, { SecondaryButton } from './Button'
import VoteCommentModal from './VoteCommentModal'
import { getProgramVersionForRealm } from '@models/registry/api'
import useVotePluginsClientStore from 'stores/useVotePluginsClientStore'
import { useRouter } from 'next/router'
import useNftPluginStore from 'NftVotePlugin/store/nftPluginStore'
import { REALM_ID as PYTH_REALM_ID } from 'pyth-staking-api'
import { isYesVote } from '@models/voteRecords'
import Tooltip from '@components/Tooltip'
import { VotingClientType } from '@utils/uiTypes/VotePlugin'
const VotePanel = () => {
const [showVoteModal, setShowVoteModal] = useState(false)
const [vote, setVote] = useState<YesNoVote | null>(null)
const client = useVotePluginsClientStore(
(s) => s.state.currentRealmVotingClient
)
const [isLoading, setIsLoading] = useState(false)
const router = useRouter()
const { pk } = router.query
const {
governance,
proposal,
voteRecordsByVoter,
tokenRole,
} = useWalletStore((s) => s.selectedProposal)
const {
ownTokenRecord,
ownCouncilTokenRecord,
realm,
realmInfo,
ownVoterWeight,
} = useRealm()
const wallet = useWalletStore((s) => s.current)
const connection = useWalletStore((s) => s.connection)
const connected = useWalletStore((s) => s.connected)
const refetchProposals = useWalletStore((s) => s.actions.refetchProposals)
const fetchProposal = useWalletStore((s) => s.actions.fetchProposal)
const hasVoteTimeExpired = useHasVoteTimeExpired(governance, proposal!)
const maxVoterWeight =
useNftPluginStore((s) => s.state.maxVoteRecord)?.pubkey || undefined
// Handle state based on if a delegated wallet has already voted or not
const ownVoteRecord =
tokenRole === GoverningTokenRole.Community && ownTokenRecord
? voteRecordsByVoter[
ownTokenRecord.account.governingTokenOwner.toBase58()
]
: ownCouncilTokenRecord
? voteRecordsByVoter[
ownCouncilTokenRecord.account.governingTokenOwner.toBase58()
]
: wallet?.publicKey && voteRecordsByVoter[wallet.publicKey.toBase58()]
const voterTokenRecord =
tokenRole === GoverningTokenRole.Community
? ownTokenRecord
: ownCouncilTokenRecord
const isVoteCast = ownVoteRecord !== undefined
const isVoting =
proposal?.account.state === ProposalState.Voting && !hasVoteTimeExpired
const hasMinAmountToVote =
voterTokenRecord &&
ownVoterWeight.hasMinAmountToVote(
voterTokenRecord.account.governingTokenMint
)
const isVoteEnabled =
connected && isVoting && !isVoteCast && hasMinAmountToVote
const isWithdrawEnabled =
connected &&
ownVoteRecord &&
!ownVoteRecord?.account.isRelinquished &&
proposal &&
(proposal!.account.state === ProposalState.Voting ||
proposal!.account.state === ProposalState.Completed ||
proposal!.account.state === ProposalState.Cancelled ||
proposal!.account.state === ProposalState.Succeeded ||
proposal!.account.state === ProposalState.Executing ||
proposal!.account.state === ProposalState.Defeated)
const submitRelinquishVote = async () => {
const rpcContext = new RpcContext(
proposal!.owner,
getProgramVersionForRealm(realmInfo!),
wallet!,
connection.current,
connection.endpoint
)
try {
setIsLoading(true)
const instructions: TransactionInstruction[] = []
if (
proposal?.account.state === ProposalState.Voting &&
hasVoteTimeExpired
) {
await withFinalizeVote(
instructions,
realmInfo!.programId,
getProgramVersionForRealm(realmInfo!),
realm!.pubkey,
proposal.account.governance,
proposal.pubkey,
proposal.account.tokenOwnerRecord,
proposal.account.governingTokenMint,
maxVoterWeight
)
}
await relinquishVote(
rpcContext,
realm!.pubkey,
proposal!,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
voterTokenRecord!.pubkey,
ownVoteRecord!.pubkey,
instructions,
client
)
await refetchProposals()
if (pk) {
fetchProposal(pk)
}
} catch (ex) {
console.error("Can't relinquish vote", ex)
}
setIsLoading(false)
}
const handleShowVoteModal = (vote: YesNoVote) => {
setVote(vote)
setShowVoteModal(true)
}
const handleCloseShowVoteModal = useCallback(() => {
setShowVoteModal(false)
}, [])
const actionLabel =
!isVoteCast || !connected
? `Cast your ${
tokenRole === GoverningTokenRole.Community ? 'community' : 'council'
} vote`
: 'Your vote'
const withdrawTooltipContent = !connected
? 'You need to connect your wallet'
: !isWithdrawEnabled
? !ownVoteRecord?.account.isRelinquished
? 'Owner vote record is not relinquished'
: 'The proposal is not in a valid state to execute this action.'
: ''
const voteTooltipContent = !connected
? 'You need to connect your wallet to be able to vote'
: !isVoting && isVoteCast
? 'Proposal is not in a voting state anymore.'
: client.clientType === VotingClientType.NftVoterClient && !voterTokenRecord
? 'You must join the Realm to be able to vote'
: !voterTokenRecord ||
!ownVoterWeight.hasMinAmountToVote(
voterTokenRecord.account.governingTokenMint
)
? 'You dont have governance power to vote in this dao'
: ''
const notVisibleStatesForNotConnectedWallet = [
ProposalState.Cancelled,
ProposalState.Succeeded,
ProposalState.Draft,
ProposalState.Completed,
]
const isVisibleToWallet = !connected
? !hasVoteTimeExpired &&
typeof notVisibleStatesForNotConnectedWallet.find(
(x) => x === proposal?.account.state
) === 'undefined'
: !ownVoteRecord?.account.isRelinquished
const isPanelVisible = (isVoting || isVoteCast) && isVisibleToWallet
const didNotVote =
!!proposal &&
!isVoting &&
proposal.account.state !== ProposalState.Cancelled &&
proposal.account.state !== ProposalState.Draft &&
!isVoteCast &&
isVisibleToWallet
//Todo: move to own components with refactor to dao folder structure
const isPyth = realmInfo?.realmId.toBase58() === PYTH_REALM_ID.toBase58()
const isRelinquishVotePanelVisible = !(
isPyth &&
isVoteCast &&
connected &&
!isVoting
)
return (
<>
{isPanelVisible && isRelinquishVotePanelVisible && (
<div className="bg-bkg-2 p-4 md:p-6 rounded-lg space-y-4">
<div className="flex flex-col items-center justify-center">
<h3 className="text-center">{actionLabel}</h3>
{isVoteCast &&
connected &&
ownVoteRecord &&
(isYesVote(ownVoteRecord.account) ? (
<Tooltip content={`You voted "Yes"`}>
<div className="flex flex-row items-center justify-center rounded-full border border-[#8EFFDD] p-2 mt-2">
<ThumbUpIcon className="h-4 w-4 fill-[#8EFFDD]" />
</div>
</Tooltip>
) : (
<Tooltip content={`You voted "No"`}>
<div className="flex flex-row items-center justify-center rounded-full border border-[#FF7C7C] p-2 mt-2">
<ThumbDownIcon className="h-4 w-4 fill-[#FF7C7C]" />
</div>
</Tooltip>
))}
</div>
<div className="items-center justify-center flex w-full gap-5">
{isVoteCast && connected ? (
<div className="flex flex-col gap-6 items-center">
{isVoting && (
<SecondaryButton
className="min-w-[200px]"
isLoading={isLoading}
tooltipMessage={withdrawTooltipContent}
onClick={() => submitRelinquishVote()}
disabled={!isWithdrawEnabled || isLoading}
>
Withdraw
</SecondaryButton>
)}
</div>
) : (
<>
{isVoting && (
<div className="w-full flex justify-between items-center gap-5">
<Button
tooltipMessage={voteTooltipContent}
className="w-1/2"
onClick={() => handleShowVoteModal(YesNoVote.Yes)}
disabled={!isVoteEnabled}
>
<div className="flex flex-row items-center justify-center">
<ThumbUpIcon className="h-4 w-4 mr-2" />
Vote Yes
</div>
</Button>
<Button
tooltipMessage={voteTooltipContent}
className="w-1/2"
onClick={() => handleShowVoteModal(YesNoVote.No)}
disabled={!isVoteEnabled}
>
<div className="flex flex-row items-center justify-center">
<ThumbDownIcon className="h-4 w-4 mr-2" />
Vote No
</div>
</Button>
</div>
)}
</>
)}
</div>
{showVoteModal ? (
<VoteCommentModal
isOpen={showVoteModal}
onClose={handleCloseShowVoteModal}
vote={vote!}
voterTokenRecord={voterTokenRecord!}
/>
) : null}
</div>
)}
{didNotVote && (
<div className="bg-bkg-2 p-4 md:p-6 rounded-lg flex flex-col items-center justify-center">
<h3 className="text-center mb-0">You did not vote</h3>
<Tooltip content="You did not vote on this proposal">
<BanIcon className="h-[34px] w-[34px] fill-white/50 mt-2" />
</Tooltip>
</div>
)}
</>
)
}
export default VotePanel

View File

@ -0,0 +1,115 @@
import { VoteKind } from '@solana/spl-governance'
import { useState } from 'react'
import { ThumbUpIcon, ThumbDownIcon } from '@heroicons/react/solid'
import Button from '../Button'
import VoteCommentModal from '../VoteCommentModal'
import {
useIsVoting,
useProposalVoteRecordQuery,
useVoterTokenRecord,
useVotingPop,
} from './hooks'
import useRealm from '@hooks/useRealm'
import { VotingClientType } from '@utils/uiTypes/VotePlugin'
import useVotePluginsClientStore from 'stores/useVotePluginsClientStore'
import useWalletStore from 'stores/useWalletStore'
const useCanVote = () => {
const client = useVotePluginsClientStore(
(s) => s.state.currentRealmVotingClient
)
const { ownVoterWeight } = useRealm()
const connected = useWalletStore((s) => s.connected)
const { data: ownVoteRecord } = useProposalVoteRecordQuery('electoral')
const voterTokenRecord = useVoterTokenRecord()
const isVoteCast = !!ownVoteRecord?.found
const hasMinAmountToVote =
voterTokenRecord &&
ownVoterWeight.hasMinAmountToVote(
voterTokenRecord.account.governingTokenMint
)
const canVote =
connected &&
!(
client.clientType === VotingClientType.NftVoterClient && !voterTokenRecord
) &&
!isVoteCast &&
hasMinAmountToVote
const voteTooltipContent = !connected
? 'You need to connect your wallet to be able to vote'
: client.clientType === VotingClientType.NftVoterClient && !voterTokenRecord
? 'You must join the Realm to be able to vote'
: !hasMinAmountToVote
? 'You dont have governance power to vote in this dao'
: ''
return [canVote, voteTooltipContent] as const
}
export const CastVoteButtons = () => {
const [showVoteModal, setShowVoteModal] = useState(false)
const [vote, setVote] = useState<'yes' | 'no' | null>(null)
const votingPop = useVotingPop()
const voterTokenRecord = useVoterTokenRecord()
const [canVote, tooltipContent] = useCanVote()
const { data: ownVoteRecord } = useProposalVoteRecordQuery('electoral')
const isVoteCast = !!ownVoteRecord?.found
const isVoting = useIsVoting()
return isVoting && !isVoteCast ? (
<div className="bg-bkg-2 p-4 md:p-6 rounded-lg space-y-4">
<div className="flex flex-col items-center justify-center">
<h3 className="text-center">Cast your {votingPop} vote</h3>
</div>
<div className="items-center justify-center flex w-full gap-5">
<div className="w-full flex justify-between items-center gap-5">
<Button
tooltipMessage={tooltipContent}
className="w-1/2"
onClick={() => {
setVote('yes')
setShowVoteModal(true)
}}
disabled={!canVote}
>
<div className="flex flex-row items-center justify-center">
<ThumbUpIcon className="h-4 w-4 mr-2" />
Vote Yes
</div>
</Button>
<Button
tooltipMessage={tooltipContent}
className="w-1/2"
onClick={() => {
setVote('no')
setShowVoteModal(true)
}}
disabled={!canVote}
>
<div className="flex flex-row items-center justify-center">
<ThumbDownIcon className="h-4 w-4 mr-2" />
Vote No
</div>
</Button>
</div>
</div>
{showVoteModal && vote ? (
<VoteCommentModal
isOpen={showVoteModal}
onClose={() => setShowVoteModal(false)}
vote={vote === 'yes' ? VoteKind.Approve : VoteKind.Deny}
voterTokenRecord={voterTokenRecord!}
/>
) : null}
</div>
) : null
}

View File

@ -0,0 +1,137 @@
import Button from '@components/Button'
import VoteCommentModal from '@components/VoteCommentModal'
import { BanIcon } from '@heroicons/react/solid'
import useRealm from '@hooks/useRealm'
import {
GoverningTokenRole,
VoteThresholdType,
VoteKind,
} from '@solana/spl-governance'
import { useMemo, useState } from 'react'
import useWalletStore from 'stores/useWalletStore'
import { useIsVoting, useProposalVoteRecordQuery } from './hooks'
/*
returns: undefined if loading, false if nobody can veto, 'council' if council can veto, 'community' if community can veto
*/
export const useVetoingPop = () => {
const { tokenRole, governance } = useWalletStore((s) => s.selectedProposal)
const vetoingPop = useMemo(() => {
if (governance === undefined) return undefined
return tokenRole === GoverningTokenRole.Community
? governance?.account.config.councilVetoVoteThreshold.type !==
VoteThresholdType.Disabled && 'council'
: governance?.account.config.communityVetoVoteThreshold.type !==
VoteThresholdType.Disabled && 'community'
}, [governance, tokenRole])
return vetoingPop
}
const useIsVetoable = (): undefined | boolean => {
const vetoingPop = useVetoingPop()
const isVoting = useIsVoting()
// TODO is this accurate?
if (isVoting === false) return false
if (vetoingPop === undefined) return undefined
return !!vetoingPop
}
const useUserVetoTokenRecord = () => {
const { ownTokenRecord, ownCouncilTokenRecord } = useRealm()
const vetoingPop = useVetoingPop()
const voterTokenRecord =
vetoingPop === 'community' ? ownTokenRecord : ownCouncilTokenRecord
return voterTokenRecord
}
const useCanVeto = ():
| undefined
| { canVeto: true }
| { canVeto: false; message: string } => {
const { ownVoterWeight } = useRealm()
const connected = useWalletStore((s) => s.connected)
const isVetoable = useIsVetoable()
const { data: userVetoRecord } = useProposalVoteRecordQuery('veto')
const voterTokenRecord = useUserVetoTokenRecord()
if (isVetoable === false)
return {
canVeto: false,
// (Note that users should never actually see this)
message: 'This proposal is not vetoable',
}
// Are you connected?
if (connected === false)
return { canVeto: false, message: 'You must connect your wallet' }
// Did you already veto?
if (userVetoRecord?.found)
return { canVeto: false, message: 'You already voted' }
// Do you have any voting power?
const hasMinAmountToVote =
voterTokenRecord &&
ownVoterWeight.hasMinAmountToVote(
voterTokenRecord.account.governingTokenMint
)
if (hasMinAmountToVote === undefined) return undefined
if (hasMinAmountToVote === false)
return {
canVeto: false,
message: 'You dont have governance power to vote in this dao',
}
return { canVeto: true }
}
const VetoButtons = () => {
const vetoable = useIsVetoable()
const vetoingPop = useVetoingPop()
const canVeto = useCanVeto()
const [openModal, setOpenModal] = useState(false)
const voterTokenRecord = useUserVetoTokenRecord()
const { data: userVetoRecord } = useProposalVoteRecordQuery('veto')
return vetoable &&
vetoingPop &&
voterTokenRecord &&
!userVetoRecord?.found ? (
<>
<div className="bg-bkg-2 p-4 md:p-6 rounded-lg space-y-4">
<div className="flex flex-col items-center justify-center">
<h3 className="text-center">Cast your {vetoingPop} veto vote</h3>
</div>
<div className="flex flex-col items-center justify-center">
<Button
tooltipMessage={
canVeto?.canVeto === false ? canVeto.message : undefined
}
className="w-full"
onClick={() => setOpenModal(true)}
disabled={!canVeto?.canVeto}
>
<div className="flex flex-row items-center justify-center">
<BanIcon className="h-4 w-4 mr-2" />
Veto
</div>
</Button>
</div>
</div>
{openModal ? (
<VoteCommentModal
onClose={() => setOpenModal(false)}
isOpen={openModal}
voterTokenRecord={voterTokenRecord}
vote={VoteKind.Veto}
/>
) : null}
</>
) : null
}
export default VetoButtons

View File

@ -0,0 +1,48 @@
import { ProposalState } from '@solana/spl-governance'
import { BanIcon } from '@heroicons/react/solid'
import useWalletStore from '../../stores/useWalletStore'
import Tooltip from '@components/Tooltip'
import VetoButtons from './VetoButtons'
import { CastVoteButtons } from './CastVoteButtons'
import { YouVoted } from './YouVoted'
import { useIsVoting, useProposalVoteRecordQuery } from './hooks'
const VotePanel = () => {
const { proposal } = useWalletStore((s) => s.selectedProposal)
const connected = useWalletStore((s) => s.connected)
const { data: ownVoteRecord } = useProposalVoteRecordQuery('electoral')
const isVoteCast = ownVoteRecord?.result !== undefined
const isVoting = useIsVoting()
const didNotVote =
connected &&
!!proposal &&
!isVoting &&
proposal.account.state !== ProposalState.Cancelled &&
proposal.account.state !== ProposalState.Draft &&
!isVoteCast
return (
<>
{didNotVote && (
<div className="bg-bkg-2 p-4 md:p-6 rounded-lg flex flex-col items-center justify-center">
<h3 className="text-center mb-0">You did not vote electorally</h3>
<Tooltip content="You did not vote on this proposal">
<BanIcon className="h-[34px] w-[34px] fill-white/50 mt-2" />
</Tooltip>
</div>
)}
{/* START: Note that these components control for themselves whether they are displayed and may not be visible */}
<YouVoted quorum="electoral" />
<CastVoteButtons />
<YouVoted quorum="veto" />
<VetoButtons />
{/* END */}
</>
)
}
export default VotePanel

View File

@ -0,0 +1,179 @@
import { VoteKind, withFinalizeVote } from '@solana/spl-governance'
import { TransactionInstruction } from '@solana/web3.js'
import { useState } from 'react'
import { relinquishVote } from '../../actions/relinquishVote'
import useRealm from '../../hooks/useRealm'
import { ProposalState } from '@solana/spl-governance'
import { RpcContext } from '@solana/spl-governance'
import {
ThumbUpIcon,
ThumbDownIcon,
BanIcon,
MinusCircleIcon,
} from '@heroicons/react/solid'
import useWalletStore from '../../stores/useWalletStore'
import { SecondaryButton } from '../Button'
import { getProgramVersionForRealm } from '@models/registry/api'
import useVotePluginsClientStore from 'stores/useVotePluginsClientStore'
import { useRouter } from 'next/router'
import useNftPluginStore from 'NftVotePlugin/store/nftPluginStore'
import Tooltip from '@components/Tooltip'
import {
useVoterTokenRecord,
useIsVoting,
useProposalVoteRecordQuery,
} from './hooks'
import assertUnreachable from '@utils/typescript/assertUnreachable'
export const YouVoted = ({ quorum }: { quorum: 'electoral' | 'veto' }) => {
const client = useVotePluginsClientStore(
(s) => s.state.currentRealmVotingClient
)
const router = useRouter()
const { pk } = router.query
const { proposal } = useWalletStore((s) => s.selectedProposal)
const { realm, realmInfo } = useRealm()
const wallet = useWalletStore((s) => s.current)
const connection = useWalletStore((s) => s.connection)
const connected = useWalletStore((s) => s.connected)
const refetchProposals = useWalletStore((s) => s.actions.refetchProposals)
const fetchProposal = useWalletStore((s) => s.actions.fetchProposal)
const maxVoterWeight =
useNftPluginStore((s) => s.state.maxVoteRecord)?.pubkey || undefined
const isVoting = useIsVoting()
const [isLoading, setIsLoading] = useState(false)
const { data } = useProposalVoteRecordQuery(quorum)
const ownVoteRecord = data?.result
const voterTokenRecord = useVoterTokenRecord()
const isWithdrawEnabled =
connected &&
ownVoteRecord &&
!ownVoteRecord?.account.isRelinquished &&
proposal &&
(proposal!.account.state === ProposalState.Voting ||
proposal!.account.state === ProposalState.Completed ||
proposal!.account.state === ProposalState.Cancelled ||
proposal!.account.state === ProposalState.Succeeded ||
proposal!.account.state === ProposalState.Executing ||
proposal!.account.state === ProposalState.Defeated)
const withdrawTooltipContent = !connected
? 'You need to connect your wallet'
: !isWithdrawEnabled
? !ownVoteRecord?.account.isRelinquished
? 'Owner vote record is not relinquished'
: 'The proposal is not in a valid state to execute this action.'
: ''
const submitRelinquishVote = async () => {
if (
realm === undefined ||
proposal === undefined ||
voterTokenRecord === undefined ||
ownVoteRecord === undefined ||
ownVoteRecord === null
)
return
const rpcContext = new RpcContext(
proposal!.owner,
getProgramVersionForRealm(realmInfo!),
wallet!,
connection.current,
connection.endpoint
)
try {
setIsLoading(true)
const instructions: TransactionInstruction[] = []
if (proposal !== undefined && isVoting) {
await withFinalizeVote(
instructions,
realmInfo!.programId,
getProgramVersionForRealm(realmInfo!),
realm!.pubkey,
proposal.account.governance,
proposal.pubkey,
proposal.account.tokenOwnerRecord,
proposal.account.governingTokenMint,
maxVoterWeight
)
}
await relinquishVote(
rpcContext,
realm.pubkey,
proposal,
voterTokenRecord.pubkey,
ownVoteRecord.pubkey,
instructions,
client
)
await refetchProposals()
if (pk) {
fetchProposal(pk)
}
} catch (ex) {
console.error("Can't relinquish vote", ex)
}
setIsLoading(false)
}
const vote = ownVoteRecord?.account.vote
return vote !== undefined ? (
<div className="bg-bkg-2 p-4 md:p-6 rounded-lg space-y-4">
<div className="flex flex-col items-center justify-center">
<h3 className="text-center">
{quorum === 'electoral' ? 'Your vote' : 'You voted to veto'}
</h3>
{vote.voteType === VoteKind.Approve ? (
<Tooltip content={`You voted "Yes"`}>
<div className="flex flex-row items-center justify-center rounded-full border border-[#8EFFDD] p-2 mt-2">
<ThumbUpIcon className="h-4 w-4 fill-[#8EFFDD]" />
</div>
</Tooltip>
) : vote.voteType === VoteKind.Deny ? (
<Tooltip content={`You voted "No"`}>
<div className="flex flex-row items-center justify-center rounded-full border border-[#FF7C7C] p-2 mt-2">
<ThumbDownIcon className="h-4 w-4 fill-[#FF7C7C]" />
</div>
</Tooltip>
) : vote.voteType === VoteKind.Veto ? (
<Tooltip content={`You voted "Veto"`}>
<div className="flex flex-row items-center justify-center rounded-full border border-[#FF7C7C] p-2 mt-2">
<BanIcon className="h-4 w-4 fill-[#FF7C7C]" />
</div>
</Tooltip>
) : vote.voteType === VoteKind.Abstain ? (
<Tooltip content={`You voted "Abstain"`}>
<div className="flex flex-row items-center justify-center rounded-full border border-gray-400 p-2 mt-2">
<MinusCircleIcon className="h-4 w-4 fill-gray-400" />
</div>
</Tooltip>
) : (
assertUnreachable(vote.voteType)
)}
</div>
{isVoting && (
<div className="items-center justify-center flex w-full gap-5">
<div className="flex flex-col gap-6 items-center">
<SecondaryButton
className="min-w-[200px]"
isLoading={isLoading}
tooltipMessage={withdrawTooltipContent}
onClick={() => submitRelinquishVote()}
disabled={!isWithdrawEnabled || isLoading}
>
Withdraw
</SecondaryButton>
</div>
</div>
)}
</div>
) : null
}

View File

@ -0,0 +1,66 @@
import {
useAddressQuery_CommunityTokenOwner,
useAddressQuery_CouncilTokenOwner,
} from '@hooks/queries/addresses/tokenOwner'
import { useAddressQuery_SelectedProposalVoteRecord } from '@hooks/queries/addresses/voteRecord'
import { useVoteRecordByPubkeyQuery } from '@hooks/queries/voteRecord'
import { useHasVoteTimeExpired } from '@hooks/useHasVoteTimeExpired'
import useRealm from '@hooks/useRealm'
import { ProposalState, GoverningTokenRole } from '@solana/spl-governance'
import useWalletStore from 'stores/useWalletStore'
export const useIsVoting = () => {
const { governance, proposal } = useWalletStore((s) => s.selectedProposal)
const hasVoteTimeExpired = useHasVoteTimeExpired(governance, proposal!)
const isVoting =
proposal?.account.state === ProposalState.Voting && !hasVoteTimeExpired
return isVoting
}
export const useVotingPop = () => {
const { tokenRole } = useWalletStore((s) => s.selectedProposal)
const votingPop =
tokenRole === GoverningTokenRole.Community ? 'community' : 'council'
return votingPop
}
export const useVoterTokenRecord = () => {
const { tokenRole } = useWalletStore((s) => s.selectedProposal)
const { ownTokenRecord, ownCouncilTokenRecord } = useRealm()
const voterTokenRecord =
tokenRole === GoverningTokenRole.Community
? ownTokenRecord
: ownCouncilTokenRecord
return voterTokenRecord
}
export const useProposalVoteRecordQuery = (quorum: 'electoral' | 'veto') => {
const tokenRole = useWalletStore((s) => s.selectedProposal.tokenRole)
const community = useAddressQuery_CommunityTokenOwner()
const council = useAddressQuery_CouncilTokenOwner()
const electoral =
tokenRole === undefined
? undefined
: tokenRole === GoverningTokenRole.Community
? community
: council
const veto =
tokenRole === undefined
? undefined
: tokenRole === GoverningTokenRole.Community
? council
: community
const selectedTokenRecord = quorum === 'electoral' ? electoral : veto
const pda = useAddressQuery_SelectedProposalVoteRecord(
selectedTokenRecord?.data
)
return useVoteRecordByPubkeyQuery(pda.data)
}

View File

@ -0,0 +1 @@
export { default } from './VotePanel'

View File

@ -1,47 +1,81 @@
import { XCircleIcon, CheckCircleIcon } from '@heroicons/react/outline'
import { BanIcon } from '@heroicons/react/solid'
import useProposal from '@hooks/useProposal'
import useProposalVotes from '@hooks/useProposalVotes'
import { ProposalState } from '@solana/spl-governance'
import { useVetoingPop } from './VotePanel/VetoButtons'
type VoteResultStatusProps = {
progress: number
votePassed: boolean | undefined
yesVotesRequired: number
const VetoResult = () => {
const vetoingPop = useVetoingPop()
return (
<div className="bg-bkg-1 flex items-center p-3 rounded-md">
<BanIcon className="h-5 mr-1.5 text-red w-5" />
<h4 className="mb-0">Vetoed by the {vetoingPop}</h4>
</div>
)
}
const VoteResultStatus = ({
progress,
votePassed,
yesVotesRequired,
}: VoteResultStatusProps) => {
return votePassed ? (
<div className="bg-bkg-1 flex items-center p-3 rounded-md">
<CheckCircleIcon className="h-5 mr-1.5 text-green w-5" />
<h4 className="mb-0">The proposal has passed</h4>
</div>
) : (
const ApprovalResult = () => (
<div className="bg-bkg-1 flex items-center p-3 rounded-md">
<CheckCircleIcon className="h-5 mr-1.5 text-green w-5" />
<h4 className="mb-0">The proposal has passed</h4>
</div>
)
const FailResult = () => {
const { proposal } = useProposal()
const voteData = useProposalVotes(proposal?.account)
return voteData.yesVotesRequired === undefined ? null : (
<div className="bg-bkg-1 p-3 rounded-md">
<div className="flex items-center">
<XCircleIcon className="h-5 mr-1.5 text-red w-5" />
<div>
<h4 className="mb-0">The proposal has failed</h4>
<p className="mb-0 text-fgd-2">{`${yesVotesRequired?.toLocaleString(
<p className="mb-0 text-fgd-2">{`${voteData.yesVotesRequired.toLocaleString(
undefined,
{
maximumFractionDigits: 0,
}
)} more Yes vote${yesVotesRequired > 1 ? 's' : ''} were needed`}</p>
)} more Yes vote${
voteData.yesVotesRequired > 1 ? 's' : ''
} were needed`}</p>
</div>
</div>
<div className="bg-bkg-4 h-2 flex flex-grow mt-2.5 rounded w-full">
<div
style={{
width: `${progress}%`,
width: `${voteData.yesVoteProgress}%`,
}}
className={`${
progress >= 100 ? 'bg-green' : 'bg-fgd-3'
} flex rounded`}
className={`bg-fgd-3 flex rounded`}
></div>
</div>
</div>
)
}
const VoteResultStatus = () => {
const { proposal } = useProposal()
const status =
proposal &&
(proposal.account.state === ProposalState.Completed ||
proposal.account.state === ProposalState.Executing ||
proposal.account.state === ProposalState.SigningOff ||
proposal.account.state === ProposalState.Succeeded ||
proposal.account.state === ProposalState.ExecutingWithErrors
? 'approved'
: proposal.account.state === ProposalState.Vetoed
? 'vetoed'
: 'denied')
return status === undefined ? null : status === 'approved' ? (
<ApprovalResult />
) : status === 'vetoed' ? (
<VetoResult />
) : (
<FailResult />
)
}
export default VoteResultStatus

View File

@ -7,6 +7,7 @@ type VoteResultsProps = {
proposal: Proposal
}
// TODO make component display well when data is loading
const VoteResults = ({ isListView, proposal }: VoteResultsProps) => {
const {
yesVoteCount,
@ -29,7 +30,7 @@ const VoteResults = ({ isListView, proposal }: VoteResultsProps) => {
!isListView ? 'hero-text' : ''
}`}
>
{yesVoteCount.toLocaleString()}
{(yesVoteCount ?? 0).toLocaleString()}
{isListView ? (
<span className="ml-1 text-xs font-normal text-fgd-3">
{relativeYesVotes?.toFixed(1)}%
@ -49,7 +50,7 @@ const VoteResults = ({ isListView, proposal }: VoteResultsProps) => {
!isListView ? 'hero-text' : ''
}`}
>
{noVoteCount.toLocaleString()}
{(noVoteCount ?? 0).toLocaleString()}
{isListView ? (
<span className="ml-1 text-xs font-normal text-fgd-3">
{relativeNoVotes?.toFixed(1)}%

View File

@ -1,4 +1,4 @@
import ApprovalQuorum from '@components/ApprovalQuorum'
import { ApprovalProgress } from '@components/QuorumProgress'
import Button from '@components/Button'
import VoteResultsForRealmProposal from '@components/VoteResultsForRealmProposal'
import { ExternalLinkIcon } from '@heroicons/react/outline'
@ -25,104 +25,12 @@ import {
import { PublicKey, Transaction, TransactionInstruction } from '@solana/web3.js'
import { notify } from '@utils/notifications'
import { InstructionDataWithHoldUpTime } from 'actions/createProposal'
import classNames from 'classnames'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useCallback, useState } from 'react'
import useWalletStore from 'stores/useWalletStore'
import VoteProposalModal from './VoteProposalModal'
function getLabel(proposalState: ProposalState, hasVotingExpired?: boolean) {
switch (proposalState) {
case ProposalState.Cancelled:
return 'Cancelled'
case ProposalState.Completed:
return 'Completed'
case ProposalState.Defeated:
return 'Defeated'
case ProposalState.Draft:
return 'Draft'
case ProposalState.Executing:
return 'Executable'
case ProposalState.ExecutingWithErrors:
return 'Executing w/ errors'
case ProposalState.SigningOff:
return 'Signing off'
case ProposalState.Succeeded:
return 'Completed'
case ProposalState.Voting:
return hasVotingExpired ? 'Finalizing' : 'Voting'
}
}
function getTextColor(
proposalState: ProposalState,
hasVotingExpired?: boolean
) {
switch (proposalState) {
case ProposalState.Cancelled:
case ProposalState.Draft:
return 'text-white'
case ProposalState.Completed:
return 'text-[#8EFFDD]'
case ProposalState.Defeated:
case ProposalState.ExecutingWithErrors:
return 'text-[#FF7C7C]'
case ProposalState.Executing:
return 'text-[#5DC9EB]'
case ProposalState.SigningOff:
return 'text-[#F5A458]'
case ProposalState.Succeeded:
return 'text-[#8EFFDD]'
case ProposalState.Voting:
return hasVotingExpired
? 'bg-gradient-to-r from-[#00C2FF] via-[#00E4FF] to-[#87F2FF] bg-clip-text text-transparent'
: 'text-[#8EFFDD]'
}
}
function getBorderColor(
proposalState: ProposalState,
hasVotingExpired?: boolean
) {
switch (proposalState) {
case ProposalState.Cancelled:
case ProposalState.Completed:
case ProposalState.Defeated:
case ProposalState.ExecutingWithErrors:
return 'border-transparent'
case ProposalState.Executing:
return 'border-[#5DC9EB]'
case ProposalState.Draft:
return 'border-white'
case ProposalState.SigningOff:
return 'border-[#F5A458]'
case ProposalState.Succeeded:
return 'border-transparent'
case ProposalState.Voting:
return hasVotingExpired ? 'border-[#5DC9EB]' : 'border-[#8EFFDD]'
}
}
function getOpacity(proposalState: ProposalState) {
switch (proposalState) {
case ProposalState.Cancelled:
case ProposalState.Completed:
case ProposalState.Defeated:
case ProposalState.ExecutingWithErrors:
return 'opacity-70'
case ProposalState.Draft:
return ''
case ProposalState.SigningOff:
return ''
case ProposalState.Succeeded:
return 'opacity-70'
default:
return ''
}
}
import ProposalStateBadge from '@components/ProposalStateBadge'
interface Props {
voteRecord?: ProgramAccount<VoteRecord>
@ -286,23 +194,7 @@ export default function ProposalDetails({
</span>
</p>
</div>
<div
className={classNames(
'border',
'inline-flex',
'min-w-max',
'items-center',
'px-2',
'py-1',
'rounded-full',
'text-xs',
getBorderColor(proposal.account.state, hasVoteTimeExpired),
getOpacity(proposal.account.state),
getTextColor(proposal.account.state, hasVoteTimeExpired)
)}
>
{getLabel(proposal.account.state, hasVoteTimeExpired)}
</div>
<ProposalStateBadge proposal={proposal.account} />
</div>
<div className="flex flex-col lg:flex-row space-y-1 lg:space-y-0 lg:space-x-3">
<div className="flex-1">
@ -315,9 +207,9 @@ export default function ProposalDetails({
</div>
<div className="hidden lg:block self-stretch w-0.5 bg-fgd-4" />
<div className="flex-1">
<ApprovalQuorum
<ApprovalProgress
progress={voteData.yesVoteProgress}
yesVotesRequired={voteData.yesVotesRequired}
votesRequired={voteData.yesVotesRequired}
/>
</div>
</div>

View File

@ -0,0 +1,80 @@
import useRealm from '@hooks/useRealm'
import { getTokenOwnerRecordAddress } from '@solana/spl-governance'
import { PublicKey } from '@solana/web3.js'
import { useQuery } from '@tanstack/react-query'
import useWalletStore from 'stores/useWalletStore'
export const useAddressQuery_CouncilTokenOwner = () => {
const { realm } = useRealm()
const wallet = useWalletStore((x) => x.current)
const selectedCouncilDelegator = useWalletStore(
(s) => s.selectedCouncilDelegate
)
// if we have a council token delegator selected (this is rare), use that. otherwise use user wallet.
const owner =
selectedCouncilDelegator !== undefined
? new PublicKey(selectedCouncilDelegator)
: wallet?.publicKey ?? undefined
return useAdressQuery_TokenOwnerRecord(
realm?.owner,
realm?.pubkey,
realm?.account.config.councilMint,
owner
)
}
export const useAddressQuery_CommunityTokenOwner = () => {
const { realm } = useRealm()
const wallet = useWalletStore((x) => x.current)
const selectedCommunityDelegator = useWalletStore(
(s) => s.selectedCommunityDelegate
)
// if we have a community token delegator selected (this is rare), use that. otherwise use user wallet.
const owner =
selectedCommunityDelegator !== undefined
? new PublicKey(selectedCommunityDelegator)
: // I wanted to eliminate `null` as a possible type
wallet?.publicKey ?? undefined
return useAdressQuery_TokenOwnerRecord(
realm?.owner,
realm?.pubkey,
realm?.account.communityMint,
owner
)
}
export const useAdressQuery_TokenOwnerRecord = (
programId?: PublicKey,
realmPk?: PublicKey,
governingTokenMint?: PublicKey,
owner?: PublicKey
) => {
const enabled =
owner !== undefined &&
governingTokenMint !== undefined &&
realmPk !== undefined &&
programId !== undefined
return useQuery({
queryKey: enabled
? ['TokenOwnerAddress', [programId, realmPk, governingTokenMint, owner]]
: undefined,
queryFn: async () => {
if (!enabled) throw new Error()
return getTokenOwnerRecordAddress(
programId,
realmPk,
governingTokenMint,
owner
)
},
enabled,
// Staletime is zero by default, so queries get refetched often. PDAs will never go stale.
staleTime: Number.MAX_SAFE_INTEGER,
})
}

View File

@ -0,0 +1,54 @@
import useRealm from '@hooks/useRealm'
import { getVoteRecordAddress } from '@solana/spl-governance'
import { PublicKey } from '@solana/web3.js'
import { useQuery } from '@tanstack/react-query'
import { useRouter } from 'next/router'
export const useAddressQuery_SelectedProposalVoteRecord = (
tokenOwnerRecordAddress?: PublicKey
) => {
const router = useRouter()
const { pk } = router.query
const { realm } = useRealm()
const programId = realm?.owner // TODO make me cached plz
const proposalAddress = typeof pk === 'string' ? new PublicKey(pk) : undefined
return useAddressQuery_VoteRecord(
programId,
proposalAddress,
tokenOwnerRecordAddress
)
}
export const useAddressQuery_VoteRecord = (
programId?: PublicKey,
proposalAddress?: PublicKey,
tokenOwnerRecordAddress?: PublicKey
) => {
const enabled =
programId !== undefined &&
proposalAddress !== undefined &&
tokenOwnerRecordAddress !== undefined
return useQuery({
queryKey: enabled
? [
'VoteRecordAddress',
[programId, proposalAddress, tokenOwnerRecordAddress],
]
: undefined,
queryFn: () => {
if (!enabled) throw new Error()
return getVoteRecordAddress(
programId,
proposalAddress,
tokenOwnerRecordAddress
)
},
enabled,
// Staletime is zero by default, so queries get refetched often. PDAs will never go stale.
staleTime: Number.MAX_SAFE_INTEGER,
})
}

View File

@ -0,0 +1,48 @@
import { getVoteRecord } from '@solana/spl-governance'
import { Connection, PublicKey } from '@solana/web3.js'
import { useQuery } from '@tanstack/react-query'
import asFindable from '@utils/queries/asFindable'
import useWalletStore from 'stores/useWalletStore'
import { useAddressQuery_SelectedProposalVoteRecord } from './addresses/voteRecord'
import queryClient from './queryClient'
export const voteRecordQueryKeys = {
all: ['VoteRecord'],
byPubkey: (k: PublicKey) => [...voteRecordQueryKeys.all, k.toString()],
}
// currently unused
export const useVoteRecordByTokenOwnerRecordQuery = (
tokenOwnerRecordAddress?: PublicKey
) => {
const pda = useAddressQuery_SelectedProposalVoteRecord(
tokenOwnerRecordAddress
)
const query = useVoteRecordByPubkeyQuery(pda.data)
return query
}
export const useVoteRecordByPubkeyQuery = (pubkey?: PublicKey) => {
const connection = useWalletStore((s) => s.connection)
const enabled = pubkey !== undefined
const query = useQuery({
queryKey: enabled ? voteRecordQueryKeys.byPubkey(pubkey) : undefined,
queryFn: async () => {
if (!enabled) throw new Error()
return asFindable(getVoteRecord)(connection.current, pubkey)
},
enabled,
})
return query
}
export const fetchVoteRecordByPubkey = (
connection: Connection,
pubkey: PublicKey
) =>
queryClient.fetchQuery({
queryKey: voteRecordQueryKeys.byPubkey(pubkey),
queryFn: () => asFindable(getVoteRecord)(connection, pubkey),
})

View File

@ -2,13 +2,16 @@ import { Proposal } from '@solana/spl-governance'
import useNftPluginStore from 'NftVotePlugin/store/nftPluginStore'
import { getProposalMaxVoteWeight } from '../models/voteWeights'
import { calculatePct, fmtTokenAmount } from '../utils/formatting'
import useProgramVersion from './useProgramVersion'
import useRealm from './useRealm'
// TODO support council plugins
export default function useProposalVotes(proposal?: Proposal) {
const { realm, mint, councilMint, governances } = useRealm()
const maxVoteRecord = useNftPluginStore((s) => s.state.maxVoteRecord)
const governance =
proposal && governances[proposal.governance?.toBase58()]?.account
const programVersion = useProgramVersion()
const proposalMint =
proposal?.governingTokenMint.toBase58() ===
@ -18,23 +21,31 @@ export default function useProposalVotes(proposal?: Proposal) {
// TODO: optimize using memo
if (!realm || !proposal || !governance || !proposalMint)
return {
voteThresholdPct: 100,
yesVotePct: 0,
yesVoteProgress: 0,
yesVoteCount: 0,
noVoteCount: 0,
minimumYesVotes: 0,
yesVotesRequired: 0,
_programVersion: undefined,
voteThresholdPct: undefined,
yesVotePct: undefined,
yesVoteProgress: undefined,
yesVoteCount: undefined,
noVoteCount: undefined,
minimumYesVotes: undefined,
yesVotesRequired: undefined,
relativeNoVotes: undefined,
relativeYesVotes: undefined,
}
const isCommunityVote =
proposal?.governingTokenMint.toBase58() ===
realm?.account.communityMint.toBase58()
const isPluginCommunityVoting = maxVoteRecord && isCommunityVote
const voteThresholdPct =
(proposal.isVoteFinalized() && proposal.voteThreshold?.value) ||
governance.config.communityVoteThreshold.value!
const voteThresholdPct = isCommunityVote
? governance.config.communityVoteThreshold.value
: governance.config.councilVoteThreshold.value
if (voteThresholdPct === undefined)
throw new Error(
'Proposal has no vote threshold (this shouldnt be possible)'
)
// note this can be WRONG if the proposal status is vetoed
const maxVoteWeight = isPluginCommunityVoting
? maxVoteRecord.account.maxVoterWeight
: getProposalMaxVoteWeight(realm.account, proposal, proposalMint)
@ -67,7 +78,7 @@ export default function useProposalVotes(proposal?: Proposal) {
? Math.ceil(rawYesVotesRequired)
: rawYesVotesRequired
return {
const results = {
voteThresholdPct,
yesVotePct,
yesVoteProgress,
@ -78,4 +89,79 @@ export default function useProposalVotes(proposal?: Proposal) {
minimumYesVotes,
yesVotesRequired,
}
// @asktree: you may be asking yourself, "is this different from the more succinct way to write this?"
// the answer is yes, in typescript it is different and this lets us use discriminated unions properly.
if (programVersion === 1)
return {
_programVersion: programVersion,
...results,
}
if (programVersion === 2)
return {
_programVersion: programVersion,
...results,
}
// VETOS
const vetoThreshold = isCommunityVote
? governance.config.councilVetoVoteThreshold
: governance.config.communityVetoVoteThreshold
if (vetoThreshold.value === undefined)
return {
_programVersion: programVersion,
...results,
veto: undefined,
}
const vetoMintInfo = isCommunityVote ? councilMint : mint
const vetoMintPk = isCommunityVote
? realm.account.config.councilMint
: realm.account.communityMint
// This represents an edge case where councilVetoVoteThreshold is defined but there is no councilMint
if (vetoMintInfo === undefined || vetoMintPk === undefined)
return {
_programVersion: programVersion,
...results,
veto: undefined,
}
const isPluginCommunityVeto = maxVoteRecord && !isCommunityVote
const vetoMaxVoteWeight = isPluginCommunityVeto
? maxVoteRecord.account.maxVoterWeight
: getProposalMaxVoteWeight(
realm.account,
proposal,
vetoMintInfo,
vetoMintPk
)
const vetoVoteCount = fmtTokenAmount(
proposal.vetoVoteWeight,
vetoMintInfo.decimals
)
const vetoVoteProgress = calculatePct(
proposal.vetoVoteWeight,
vetoMaxVoteWeight
)
const minimumVetoVotes =
fmtTokenAmount(vetoMaxVoteWeight, vetoMintInfo.decimals) *
(vetoThreshold.value / 100)
const vetoVotesRequired = minimumVetoVotes - vetoVoteCount
return {
_programVersion: programVersion,
...results,
veto: {
votesRequired: vetoVotesRequired,
voteCount: vetoVoteCount,
voteProgress: vetoVoteProgress,
},
}
}

View File

@ -132,8 +132,7 @@ export default function useRealm() {
return tokenRecords[wallet.publicKey.toBase58()]
}
return undefined
// eslint-disable-next-line react-hooks/exhaustive-deps -- TODO please fix, it can cause difficult bugs. You might wanna check out https://bobbyhadz.com/blog/react-hooks-exhaustive-deps for info. -@asktree
}, [tokenRecords, wallet, connected, selectedCommunityDelegate])
}, [tokenRecords, wallet, selectedCommunityDelegate])
// returns array of community tokenOwnerRecords that connected wallet has been delegated
const ownDelegateTokenRecords = useMemo(() => {

View File

@ -28,6 +28,7 @@ const VotingFilter: Filters = {
ExecutingWithErrors: false,
SigningOff: false,
Voting: true,
Vetoed: false,
}
export default function useRealmProposals(

View File

@ -4,6 +4,7 @@ import {
TokenOwnerRecord,
Realm,
Proposal,
VoteKind,
} from '@solana/spl-governance'
import { MintInfo } from '@solana/spl-token'
import BN from 'bn.js'
@ -56,11 +57,15 @@ export function buildTopVoters(
): VoterDisplayData[] {
const maxVote = calculateMaxVoteScore(realm, proposal, governingTokenMint)
const electoralVotes = voteRecords.filter(
(x) => x.account.vote?.voteType !== VoteKind.Veto
)
const undecidedData = tokenOwnerRecords
.filter(
(tokenOwnerRecord) =>
!tokenOwnerRecord.account.governingTokenDepositAmount.isZero() &&
!voteRecords.some(
!electoralVotes.some(
(voteRecord) =>
voteRecord.account.governingTokenOwner.toBase58() ===
tokenOwnerRecord.account.governingTokenOwner.toBase58()
@ -76,7 +81,7 @@ export function buildTopVoters(
)
)
const noVoteData = voteRecords
const noVoteData = electoralVotes
.filter((record) => record.account.getNoVoteWeight()?.gt(ZERO))
.map((record) =>
buildResults(
@ -88,7 +93,7 @@ export function buildTopVoters(
)
)
const yesVoteData = voteRecords
const yesVoteData = electoralVotes
.filter((record) => record.account.getYesVoteWeight()?.gt(ZERO))
.map((record) =>
buildResults(

View File

@ -10,16 +10,7 @@ export function isYesVote(voteRecord: VoteRecord) {
return voteRecord.voteWeight?.yes && !voteRecord.voteWeight.yes.isZero()
}
case GovernanceAccountType.VoteRecordV2: {
switch (voteRecord.vote?.voteType) {
case VoteKind.Approve: {
return true
}
case VoteKind.Deny: {
return false
}
default:
throw new Error('Invalid voteKind')
}
return voteRecord.vote?.voteType === VoteKind.Approve
}
default:
throw new Error(`Invalid account type ${voteRecord.accountType} `)

View File

@ -551,7 +551,7 @@ export class SimpleGatedVoterWeight implements VoterWeightInterface {
}
}
/// Returns max VoteWeight for given mint and max source
/** Returns max VoteWeight for given mint and max source */
export function getMintMaxVoteWeight(
mint: MintInfo,
maxVoteWeightSource: MintMaxVoteWeightSource
@ -574,11 +574,13 @@ export function getMintMaxVoteWeight(
}
}
/// Returns max vote weight for a proposal
/** Returns max vote weight for a proposal */
export function getProposalMaxVoteWeight(
realm: Realm,
proposal: Proposal,
governingTokenMint: MintInfo
governingTokenMint: MintInfo,
// For vetos we want to override the proposal.governingTokenMint
governingTokenMintPk?: PublicKey
) {
// For finalized proposals the max is stored on the proposal in case it can change in the future
if (proposal.isVoteFinalized() && proposal.maxVoteWeight) {
@ -587,7 +589,7 @@ export function getProposalMaxVoteWeight(
// Council votes are currently not affected by MaxVoteWeightSource
if (
proposal.governingTokenMint.toBase58() ===
(governingTokenMintPk ?? proposal.governingTokenMint).toBase58() ===
realm.config.councilMint?.toBase58()
) {
return governingTokenMint.supply

View File

@ -101,6 +101,7 @@
"@switchboard-xyz/switchboard-v2": "0.0.110",
"@tailwindcss/line-clamp": "0.4.2",
"@tanstack/react-query": "4.14.3",
"@tanstack/react-query-devtools": "4.19.1",
"@tippyjs/react": "4.2.6",
"@types/ramda": "0.28.15",
"@urql/exchange-auth": "1.0.0",

View File

@ -60,7 +60,6 @@ const REALM = () => {
ownTokenRecord,
councilTokenOwnerRecords,
ownCouncilTokenRecord,
isNftMode,
} = useRealm()
const proposalsPerPage = 20
const [filters, setFilters] = useState<Filters>(InitialFilters)
@ -86,7 +85,6 @@ const REALM = () => {
(s) => s.state.currentRealmVotingClient
)
const wallet = useWalletStore((s) => s.current)
const connected = useWalletStore((s) => s.connected)
const connection = useWalletStore((s) => s.connection.current)
const allProposals = Object.entries(proposals).sort((a, b) =>
@ -266,14 +264,6 @@ const REALM = () => {
setIsMultiVoting(false)
}
const showMultiVote = useMemo(
() =>
realm
? realm.account.votingProposalCount > 1 && connected && !isNftMode
: false,
// eslint-disable-next-line react-hooks/exhaustive-deps -- TODO please fix, it can cause difficult bugs. You might wanna check out https://bobbyhadz.com/blog/react-hooks-exhaustive-deps for info. -@asktree
[realm, connected]
)
//Todo: move to own components with refactor to dao folder structure
const isPyth = realmInfo?.realmId.toBase58() === PYTH_REALM_ID.toBase58()
@ -382,9 +372,7 @@ const REALM = () => {
/>
</div>
<div
className={`flex lg:flex-row items-center justify-between lg:space-x-3 w-full ${
showMultiVote ? 'flex-col-reverse' : 'flex-row'
}`}
className={`flex lg:flex-row items-center justify-between lg:space-x-3 w-full flex-col-reverse`}
>
<h4 className="font-normal mb-0 text-fgd-2 whitespace-nowrap">{`${
filteredProposals.length
@ -392,25 +380,17 @@ const REALM = () => {
filteredProposals.length === 1 ? '' : 's'
}`}</h4>
<div
className={`flex items-center lg:justify-end lg:pb-0 lg:space-x-3 w-full ${
showMultiVote
? 'justify-between pb-3'
: 'justify-end'
}`}
className={`flex items-center lg:justify-end lg:pb-0 lg:space-x-3 w-full justify-between pb-3`}
>
{showMultiVote ? (
<div className="flex items-center">
<p className="mb-0 mr-1 text-fgd-3">
Multi-vote Mode
</p>
<Switch
checked={multiVoteMode}
onChange={() => {
toggleMultiVoteMode()
}}
/>
</div>
) : null}
<div className="flex items-center">
<p className="mb-0 mr-1 text-fgd-3">Batch voting</p>
<Switch
checked={multiVoteMode}
onChange={() => {
toggleMultiVoteMode()
}}
/>
</div>
<NewProposalBtn />
</div>
</div>

View File

@ -5,7 +5,7 @@ import { ChevronLeftIcon } from '@heroicons/react/solid'
import useProposal from '@hooks/useProposal'
import useVoteRecords from '@hooks/useVoteRecords'
import ProposalStateBadge from '@components/ProposalStatusBadge'
import ProposalStateBadge from '@components/ProposalStateBadge'
import ProposalTopVotersList from '@components/ProposalTopVotersList'
import ProposalTopVotersBubbleChart from '@components/ProposalTopVotersBubbleChart'
import useWalletStore from 'stores/useWalletStore'

View File

@ -2,11 +2,11 @@ import ReactMarkdown from 'react-markdown/react-markdown.min'
import remarkGfm from 'remark-gfm'
import { ExternalLinkIcon } from '@heroicons/react/outline'
import useProposal from 'hooks/useProposal'
import ProposalStateBadge from 'components/ProposalStatusBadge'
import ProposalStateBadge from '@components/ProposalStateBadge'
import { InstructionPanel } from 'components/instructions/instructionPanel'
import DiscussionPanel from 'components/chat/DiscussionPanel'
import VotePanel from 'components/VotePanel'
import ApprovalQuorum from 'components/ApprovalQuorum'
import VotePanel from '@components/VotePanel/VotePanel'
import { ApprovalProgress, VetoProgress } from '@components/QuorumProgress'
import useRealm from 'hooks/useRealm'
import useProposalVotes from 'hooks/useProposalVotes'
import ProposalTimeStatus from 'components/ProposalTimeStatus'
@ -32,23 +32,13 @@ const Proposal = () => {
const { realmInfo, symbol } = useRealm()
const { proposal, descriptionLink, governance } = useProposal()
const [description, setDescription] = useState('')
const { yesVoteProgress, yesVotesRequired } = useProposalVotes(
proposal?.account
)
const voteData = useProposalVotes(proposal?.account)
const currentWallet = useWalletStore((s) => s.current)
const showResults =
proposal &&
proposal.account.state !== ProposalState.Cancelled &&
proposal.account.state !== ProposalState.Draft
const votePassed =
proposal &&
(proposal.account.state === ProposalState.Completed ||
proposal.account.state === ProposalState.Executing ||
proposal.account.state === ProposalState.SigningOff ||
proposal.account.state === ProposalState.Succeeded ||
proposal.account.state === ProposalState.ExecutingWithErrors)
const votingEnded =
!!governance &&
!!proposal &&
@ -148,20 +138,28 @@ const Proposal = () => {
<h3 className="mb-4">Results</h3>
)}
{proposal?.account.state === ProposalState.Voting ? (
<div className="pb-3">
<ApprovalQuorum
yesVotesRequired={yesVotesRequired}
progress={yesVoteProgress}
showBg
/>
</div>
<>
<div className="pb-3">
<ApprovalProgress
votesRequired={voteData.yesVotesRequired}
progress={voteData.yesVoteProgress}
showBg
/>
</div>
{voteData._programVersion === 3 &&
voteData.veto !== undefined ? (
<div className="pb-3">
<VetoProgress
votesRequired={voteData.veto.votesRequired}
progress={voteData.veto.voteProgress}
showBg
/>
</div>
) : undefined}
</>
) : (
<div className="pb-3">
<VoteResultStatus
progress={yesVoteProgress}
votePassed={votePassed}
yesVotesRequired={yesVotesRequired}
/>
<VoteResultStatus />
</div>
)}
<VoteResults proposal={proposal.account} />

View File

@ -100,6 +100,7 @@ const MyProposalsBn = () => {
x.account.state === ProposalState.Succeeded ||
x.account.state === ProposalState.ExecutingWithErrors ||
x.account.state === ProposalState.Defeated ||
x.account.state === ProposalState.Vetoed ||
x.account.state === ProposalState.Cancelled) &&
ownVoteRecordsByProposal[x.pubkey.toBase58()] &&
!ownVoteRecordsByProposal[x.pubkey.toBase58()]?.account.isRelinquished

View File

@ -1,5 +1,5 @@
import { Filters } from '@components/ProposalFilter'
import { hasInstructions } from '@components/ProposalStatusBadge'
import { hasInstructions } from '@components/ProposalStateBadge'
import {
Governance,
ProgramAccount,
@ -76,6 +76,10 @@ export const filterProposals = (
}
}
if (!filters.Vetoed && proposal.account.state === ProposalState.Vetoed) {
return false
}
if (
!filters.Defeated &&
proposal.account.state === ProposalState.Defeated

View File

@ -0,0 +1,18 @@
const asFindable = <P extends any[], R>(f: (...p: P) => Promise<R>) => async (
...p: P
) => {
try {
return {
found: true,
result: await f(...p),
} as const
} catch (e) {
if ((e.message as string).includes('not found')) {
return { found: false, result: undefined } as const
}
return Promise.reject(e)
}
}
export default asFindable

View File

@ -0,0 +1,3 @@
export default function assertUnreachable(_: never): never {
throw new Error('An unreachability assertion was reached')
}

View File

@ -5163,11 +5163,27 @@
resolved "https://registry.yarnpkg.com/@tailwindcss/line-clamp/-/line-clamp-0.4.2.tgz#f353c5a8ab2c939c6267ac5b907f012e5ee130f9"
integrity sha512-HFzAQuqYCjyy/SX9sLGB1lroPzmcnWv1FHkIpmypte10hptf4oPUfucryMKovZh2u0uiS9U5Ty3GghWfEJGwVw==
"@tanstack/match-sorter-utils@^8.7.0":
version "8.7.0"
resolved "https://registry.yarnpkg.com/@tanstack/match-sorter-utils/-/match-sorter-utils-8.7.0.tgz#60b09a6d3d7974d5f86f1318053c1bd5a85fb0be"
integrity sha512-OgfIPMHTfuw9JGcXCCoEHWFP/eSP2eyhCYwkrFnWBM3NbUPAgOlFP11DbM7cozDRVB0XbPr1tD4pLAtWKlVUVg==
dependencies:
remove-accents "0.4.2"
"@tanstack/query-core@4.14.3":
version "4.14.3"
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.14.3.tgz#9ab6763b61d5e491816483f3aeed771c1fbd657b"
integrity sha512-1OrxZk5jSKAjTIDgH/OHa6GfVpgGDVbCVf3KHQjXLFdutK4PSSXQIyX7I1HvBR3mYKyvFPo6yXKNG7QrkwUj9g==
"@tanstack/react-query-devtools@4.19.1":
version "4.19.1"
resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-4.19.1.tgz#850058df8dba932362838c17f566bd717044449b"
integrity sha512-U63A+ly9JLPJj7ryR9omdXT3n+gS7jlExrHty4klsd/6xdUhC38CKZyZ0Gi3vctaVYRGTU8/vI+uKzBYdFqLaA==
dependencies:
"@tanstack/match-sorter-utils" "^8.7.0"
superjson "^1.10.0"
use-sync-external-store "^1.2.0"
"@tanstack/react-query@4.14.3":
version "4.14.3"
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.14.3.tgz#3bafea000480463de35721c742265730e2973550"
@ -8030,6 +8046,18 @@ cookiejar@^2.1.3:
resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.3.tgz#fc7a6216e408e74414b90230050842dacda75acc"
integrity sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==
copy-anything@^3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-3.0.3.tgz#206767156f08da0e02efd392f71abcdf79643559"
integrity sha512-fpW2W/BqEzqPp29QS+MwwfisHCQZtiduTe/m8idFo0xbti9fIZ2WVhAsCv4ggFVH3AgCkVdpoOCtQC6gBrdhjw==
dependencies:
is-what "^4.1.8"
copy-descriptor@^0.1.0:
version "0.1.1"
resolved "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz"
integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
copy-to-clipboard@^3.3.1:
version "3.3.3"
resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz#55ac43a1db8ae639a4bd99511c148cdd1b83a1b0"