283 lines
8.6 KiB
TypeScript
283 lines
8.6 KiB
TypeScript
import {
|
|
ProgramAccount,
|
|
Proposal,
|
|
VoteKind,
|
|
VoteRecord,
|
|
getGovernanceAccount,
|
|
getVoteRecordAddress,
|
|
} from '@solana/spl-governance'
|
|
import { VoteCountdown } from './VoteCountdown'
|
|
import { RawMint } from '@solana/spl-token'
|
|
import VoteResults from './VoteResult'
|
|
import QuorumProgress from './VoteProgress'
|
|
import GovernanceStore from '@store/governanceStore'
|
|
import Button from '@components/shared/Button'
|
|
import {
|
|
ArrowTopRightOnSquareIcon,
|
|
HandThumbDownIcon,
|
|
HandThumbUpIcon,
|
|
} from '@heroicons/react/20/solid'
|
|
import { BN } from '@project-serum/anchor'
|
|
import { useEffect, useState } from 'react'
|
|
import { MANGO_GOVERNANCE_PROGRAM } from 'utils/governance/constants'
|
|
import mangoStore from '@store/mangoStore'
|
|
import { castVote } from 'utils/governance/instructions/castVote'
|
|
import { useWallet } from '@solana/wallet-adapter-react'
|
|
import { relinquishVote } from 'utils/governance/instructions/relinquishVote'
|
|
import { PublicKey } from '@solana/web3.js'
|
|
import { notify } from 'utils/notifications'
|
|
import Loading from '@components/shared/Loading'
|
|
import { useTranslation } from 'next-i18next'
|
|
import { resolveProposalDescription } from 'utils/governance/tools'
|
|
|
|
enum PROCESSED_VOTE_TYPE {
|
|
APPROVE,
|
|
DENY,
|
|
RELINQUISH,
|
|
}
|
|
|
|
const ProposalCard = ({
|
|
proposal,
|
|
mangoMint,
|
|
}: {
|
|
proposal: ProgramAccount<Proposal>
|
|
mangoMint: RawMint
|
|
}) => {
|
|
const { t } = useTranslation('governance')
|
|
const connection = mangoStore((s) => s.connection)
|
|
const client = mangoStore((s) => s.client)
|
|
const governances = GovernanceStore((s) => s.governances)
|
|
const wallet = useWallet()
|
|
const voter = GovernanceStore((s) => s.voter)
|
|
const vsrClient = GovernanceStore((s) => s.vsrClient)
|
|
const updateProposals = GovernanceStore((s) => s.updateProposals)
|
|
|
|
const [processedVoteType, setProcessedVoteType] = useState<
|
|
PROCESSED_VOTE_TYPE | ''
|
|
>('')
|
|
const [voteType, setVoteType] = useState<VoteKind | undefined>(undefined)
|
|
|
|
const [voteRecordAddress, setVoteRecordAddress] = useState<PublicKey | null>(
|
|
null
|
|
)
|
|
const [isVoteCast, setIsVoteCast] = useState(false)
|
|
const [description, setDescription] = useState('')
|
|
|
|
const governance =
|
|
governances && governances[proposal.account.governance.toBase58()]
|
|
const canVote = voter.voteWeight.cmp(new BN(1)) !== -1
|
|
const descriptionLink = proposal.account.descriptionLink
|
|
|
|
//Approve 0, deny 1
|
|
const vote = async (voteType: VoteKind) => {
|
|
setProcessedVoteType(
|
|
voteType === VoteKind.Approve
|
|
? PROCESSED_VOTE_TYPE.APPROVE
|
|
: PROCESSED_VOTE_TYPE.DENY
|
|
)
|
|
try {
|
|
await castVote(
|
|
connection,
|
|
wallet,
|
|
proposal,
|
|
voter.tokenOwnerRecord!,
|
|
voteType,
|
|
vsrClient!,
|
|
client
|
|
)
|
|
await updateProposals(proposal.pubkey)
|
|
} catch (e) {
|
|
notify({
|
|
title: 'Error',
|
|
description: `${e}`,
|
|
type: 'error',
|
|
})
|
|
}
|
|
|
|
setProcessedVoteType('')
|
|
}
|
|
|
|
const submitRelinquishVote = async () => {
|
|
setProcessedVoteType(PROCESSED_VOTE_TYPE.RELINQUISH)
|
|
try {
|
|
await relinquishVote(
|
|
connection,
|
|
wallet,
|
|
proposal,
|
|
voter.tokenOwnerRecord!,
|
|
client,
|
|
voteRecordAddress!
|
|
)
|
|
await updateProposals(proposal.pubkey)
|
|
} catch (e) {
|
|
notify({
|
|
title: 'Error',
|
|
description: `${e}`,
|
|
type: 'error',
|
|
})
|
|
}
|
|
setProcessedVoteType('')
|
|
}
|
|
|
|
useEffect(() => {
|
|
const handleGetVoteRecord = async () => {
|
|
setIsVoteCast(false)
|
|
try {
|
|
await getGovernanceAccount(connection, voteRecordAddress!, VoteRecord)
|
|
setIsVoteCast(true)
|
|
// eslint-disable-next-line no-empty
|
|
} catch (e) {}
|
|
}
|
|
if (voteRecordAddress?.toBase58()) {
|
|
handleGetVoteRecord()
|
|
} else {
|
|
setIsVoteCast(false)
|
|
}
|
|
}, [voteRecordAddress, proposal.pubkey.toBase58()])
|
|
|
|
useEffect(() => {
|
|
const handleGetVoteRecordAddress = async () => {
|
|
const voteRecordAddress = await getVoteRecordAddress(
|
|
MANGO_GOVERNANCE_PROGRAM,
|
|
proposal.pubkey,
|
|
voter.tokenOwnerRecord!.pubkey!
|
|
)
|
|
setVoteRecordAddress(voteRecordAddress)
|
|
try {
|
|
const governanceAccount = await getGovernanceAccount(
|
|
connection,
|
|
voteRecordAddress,
|
|
VoteRecord
|
|
)
|
|
setIsVoteCast(true)
|
|
setVoteType(governanceAccount.account.vote?.voteType)
|
|
} catch (e) {
|
|
setIsVoteCast(false)
|
|
}
|
|
}
|
|
if (voter.tokenOwnerRecord?.pubkey.toBase58()) {
|
|
handleGetVoteRecordAddress()
|
|
} else {
|
|
setVoteRecordAddress(null)
|
|
}
|
|
}, [proposal.pubkey.toBase58(), voter.tokenOwnerRecord?.pubkey.toBase58()])
|
|
|
|
useEffect(() => {
|
|
const handleResolveDescription = async () => {
|
|
const description = await resolveProposalDescription(descriptionLink!)
|
|
setDescription(description)
|
|
}
|
|
if (descriptionLink) {
|
|
handleResolveDescription()
|
|
} else {
|
|
setDescription('')
|
|
}
|
|
}, [descriptionLink])
|
|
|
|
return governance ? (
|
|
<div
|
|
className="rounded-lg border border-th-bkg-3 p-4 md:p-6"
|
|
key={proposal.pubkey.toBase58()}
|
|
>
|
|
<div className="mb-6 flex flex-col md:flex-row md:items-start md:justify-between">
|
|
<div className="pr-6">
|
|
<h2 className="mb-2 text-lg md:text-xl">
|
|
<a
|
|
href={`https://dao.mango.markets/dao/MNGO/proposal/${proposal.pubkey.toBase58()}`}
|
|
>
|
|
<span className="mr-2">{proposal.account.name}</span>
|
|
<ArrowTopRightOnSquareIcon className="mb-1 inline-block h-4 w-4 flex-shrink-0" />
|
|
</a>
|
|
</h2>
|
|
<p className="mb-2 break-all md:mb-0">{description}</p>
|
|
</div>
|
|
<VoteCountdown
|
|
proposal={proposal.account}
|
|
governance={governance.account}
|
|
/>
|
|
</div>
|
|
<div>
|
|
{!isVoteCast ? (
|
|
<div className="flex space-x-4">
|
|
<Button
|
|
className="w-32"
|
|
onClick={() => vote(VoteKind.Approve)}
|
|
disabled={!canVote || processedVoteType !== ''}
|
|
secondary
|
|
>
|
|
<div className="flex items-center justify-center">
|
|
<HandThumbUpIcon className="mr-2 h-4 w-4" />
|
|
{processedVoteType === PROCESSED_VOTE_TYPE.APPROVE ? (
|
|
<Loading className="w-4"></Loading>
|
|
) : (
|
|
t('vote-yes')
|
|
)}
|
|
</div>
|
|
</Button>
|
|
<Button
|
|
className="w-32"
|
|
onClick={() => vote(VoteKind.Deny)}
|
|
disabled={!canVote || processedVoteType !== ''}
|
|
secondary
|
|
>
|
|
<div className="flex items-center justify-center">
|
|
<HandThumbDownIcon className="mr-2 h-4 w-4" />
|
|
{processedVoteType === PROCESSED_VOTE_TYPE.DENY ? (
|
|
<Loading className="w-4"></Loading>
|
|
) : (
|
|
t('vote-no')
|
|
)}
|
|
</div>
|
|
</Button>
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-wrap items-center">
|
|
<Button
|
|
className="mr-4 flex w-40 items-center justify-center"
|
|
disabled={processedVoteType !== ''}
|
|
secondary
|
|
onClick={() => submitRelinquishVote()}
|
|
>
|
|
{processedVoteType === PROCESSED_VOTE_TYPE.RELINQUISH ? (
|
|
<Loading className="w-4"></Loading>
|
|
) : (
|
|
t('relinquish-vote')
|
|
)}
|
|
</Button>
|
|
{voteType !== undefined ? (
|
|
<div className="my-2 flex">
|
|
<p className="mr-2">{t('current-vote')}</p>
|
|
<span className="font-bold text-th-fgd-2">
|
|
{voteType === VoteKind.Approve ? (
|
|
<span className="flex items-center">
|
|
<HandThumbUpIcon className="mr-1.5 h-3 w-3" />
|
|
{t('yes')}
|
|
</span>
|
|
) : (
|
|
<span className="flex items-center">
|
|
<HandThumbDownIcon className="mr-1.5 h-3 w-3" />
|
|
{t('no')}
|
|
</span>
|
|
)}
|
|
</span>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
)}
|
|
</div>
|
|
{mangoMint && (
|
|
<div className="mt-6 flex w-full flex-col space-y-4 border-t border-th-bkg-3 pt-4 md:flex-row md:space-y-0 md:space-x-6">
|
|
<VoteResults communityMint={mangoMint} proposal={proposal.account} />
|
|
<QuorumProgress
|
|
proposal={proposal}
|
|
governance={governance}
|
|
communityMint={mangoMint}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : null
|
|
}
|
|
|
|
export default ProposalCard
|