diff --git a/components/governance/GovernancePageWrapper.tsx b/components/governance/GovernancePageWrapper.tsx
index d3fdc3b4..671e19b4 100644
--- a/components/governance/GovernancePageWrapper.tsx
+++ b/components/governance/GovernancePageWrapper.tsx
@@ -10,7 +10,8 @@ const GovernancePageWrapper = ({ children }: { children: ReactNode }) => {
const connectionContext = GovernanceStore((s) => s.connectionContext)
const initRealm = GovernanceStore((s) => s.initRealm)
const vsrClient = GovernanceStore((s) => s.vsrClient)
- const fetchVoterWeight = GovernanceStore((s) => s.fetchVoterWeight)
+ const fetchVoter = GovernanceStore((s) => s.fetchVoter)
+ const resetVoter = GovernanceStore((s) => s.resetVoter)
const realm = GovernanceStore((s) => s.realm)
const connection = mangoStore((s) => s.connection)
@@ -32,7 +33,9 @@ const GovernancePageWrapper = ({ children }: { children: ReactNode }) => {
connectionContext?.endpoint &&
vsrClient?.program.programId.toBase58()
) {
- fetchVoterWeight(publicKey, vsrClient, connectionContext)
+ fetchVoter(publicKey, vsrClient, connectionContext)
+ } else {
+ resetVoter()
}
}, [
publicKey?.toBase58(),
diff --git a/components/governance/ListToken/ListToken.tsx b/components/governance/ListToken/ListToken.tsx
index 2e7665e0..d950a113 100644
--- a/components/governance/ListToken/ListToken.tsx
+++ b/components/governance/ListToken/ListToken.tsx
@@ -89,7 +89,7 @@ const ListToken = () => {
const loadingRealm = GovernanceStore((s) => s.loadingRealm)
const loadingVoter = GovernanceStore((s) => s.loadingVoter)
const proposals = GovernanceStore((s) => s.proposals)
- const fetchVoterWeight = GovernanceStore((s) => s.fetchVoterWeight)
+ const fetchVoter = GovernanceStore((s) => s.fetchVoter)
const connectionContext = GovernanceStore((s) => s.connectionContext)
const { t } = useTranslation(['governance'])
@@ -364,7 +364,7 @@ const ListToken = () => {
return
}
if (!wallet?.publicKey || !vsrClient || !connectionContext) return
- await fetchVoterWeight(wallet.publicKey, vsrClient, connectionContext)
+ await fetchVoter(wallet.publicKey, vsrClient, connectionContext)
if (voter.voteWeight.cmp(minVoterWeight) === -1) {
notify({
diff --git a/components/governance/ListToken/ListTokenPage.tsx b/components/governance/ListToken/ListTokenPage.tsx
new file mode 100644
index 00000000..840c12bf
--- /dev/null
+++ b/components/governance/ListToken/ListTokenPage.tsx
@@ -0,0 +1,15 @@
+import dynamic from 'next/dynamic'
+import GovernancePageWrapper from '../GovernancePageWrapper'
+
+const ListToken = dynamic(() => import('./ListToken'))
+
+const ListTokenPage = () => {
+ return (
+
+
+
+
+
+ )
+}
+export default ListTokenPage
diff --git a/components/governance/Vote/ProposalCard.tsx b/components/governance/Vote/ProposalCard.tsx
new file mode 100644
index 00000000..2c154374
--- /dev/null
+++ b/components/governance/Vote/ProposalCard.tsx
@@ -0,0 +1,267 @@
+import {
+ ProgramAccount,
+ Proposal,
+ VoteKind,
+ VoteRecord,
+ getGovernanceAccount,
+ getVoteRecordAddress,
+} from '@solana/spl-governance'
+import { VoteCountdown } from './VoteCountdown'
+import { MintInfo } 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'
+
+enum PROCESSED_VOTE_TYPE {
+ APPROVE,
+ DENY,
+ RELINQUISH,
+}
+
+const ProposalCard = ({
+ proposal,
+ mangoMint,
+}: {
+ proposal: ProgramAccount
+ mangoMint: MintInfo
+}) => {
+ 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(undefined)
+
+ const [voteRecordAddress, setVoteRecordAddress] = useState(
+ null
+ )
+ const [isVoteCast, setIsVoteCast] = useState(false)
+
+ const governance =
+ governances && governances[proposal.account.governance.toBase58()]
+ const canVote = voter.voteWeight.cmp(new BN(1)) !== -1
+
+ //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()])
+
+ return governance ? (
+
+
+
+
+
{proposal.account.descriptionLink}
+
+
+
+
+ {!isVoteCast ? (
+
+
+
+
+ ) : (
+
+
+ {voteType !== undefined ? (
+
+
{t('current-vote')}
+
+ {voteType === VoteKind.Approve ? (
+
+
+ {t('yes')}
+
+ ) : (
+
+
+ {t('no')}
+
+ )}
+
+
+ ) : null}
+
+ )}
+
+ {mangoMint && (
+
+
+
+
+ )}
+
+ ) : null
+}
+
+export default ProposalCard
diff --git a/components/governance/Vote/Vote.tsx b/components/governance/Vote/Vote.tsx
new file mode 100644
index 00000000..16c0fa2b
--- /dev/null
+++ b/components/governance/Vote/Vote.tsx
@@ -0,0 +1,117 @@
+import { ProgramAccount, Proposal, ProposalState } from '@solana/spl-governance'
+import GovernanceStore from '@store/governanceStore'
+import mangoStore from '@store/mangoStore'
+import { useEffect, useState } from 'react'
+import { isInCoolOffTime } from 'utils/governance/proposals'
+import { MintInfo } from '@solana/spl-token'
+import { MANGO_MINT } from 'utils/constants'
+import { PublicKey } from '@solana/web3.js'
+import dynamic from 'next/dynamic'
+import { fmtTokenAmount, tryGetMint } from 'utils/governance/tools'
+import { BN } from '@project-serum/anchor'
+import { MANGO_MINT_DECIMALS } from 'utils/governance/constants'
+import { useTranslation } from 'next-i18next'
+import SheenLoader from '@components/shared/SheenLoader'
+import { NoSymbolIcon } from '@heroicons/react/20/solid'
+
+const ProposalCard = dynamic(() => import('./ProposalCard'))
+const OnBoarding = dynamic(() => import('../OnBoarding'))
+
+const Vote = () => {
+ const { t } = useTranslation('governance')
+ const connection = mangoStore((s) => s.connection)
+ const governances = GovernanceStore((s) => s.governances)
+ const proposals = GovernanceStore((s) => s.proposals)
+ const loadingProposals = GovernanceStore((s) => s.loadingProposals)
+ const voter = GovernanceStore((s) => s.voter)
+ const loadingVoter = GovernanceStore((s) => s.loadingVoter)
+ const loadingRealm = GovernanceStore((s) => s.loadingRealm)
+
+ const [mangoMint, setMangoMint] = useState(null)
+ const [votingProposals, setVotingProposals] = useState<
+ ProgramAccount[]
+ >([])
+
+ useEffect(() => {
+ if (proposals) {
+ const activeProposals = Object.values(proposals).filter((x) => {
+ const governance =
+ governances && governances[x.account.governance.toBase58()]
+ const votingEnded =
+ governance && x.account.getTimeToVoteEnd(governance.account) < 0
+
+ const coolOff = isInCoolOffTime(x.account, governance?.account)
+
+ return (
+ !coolOff && !votingEnded && x.account.state === ProposalState.Voting
+ )
+ })
+ setVotingProposals(activeProposals)
+ } else {
+ setVotingProposals([])
+ }
+ }, [governances, proposals])
+
+ useEffect(() => {
+ const handleGetMangoMint = async () => {
+ const mangoMint = await tryGetMint(connection, new PublicKey(MANGO_MINT))
+ setMangoMint(mangoMint!.account)
+ }
+ handleGetMangoMint()
+ }, [])
+
+ return (
+
+
+
{t('active-proposals')}
+
+ {t('your-votes')}{' '}
+
+ {!loadingVoter
+ ? fmtTokenAmount(voter.voteWeight, MANGO_MINT_DECIMALS)
+ : 0}
+
+
+
+ {loadingProposals || loadingRealm ? (
+
+ ) : (
+ <>
+ {!loadingVoter ? (
+
+ ) : null}
+
+ {votingProposals.length ? (
+ votingProposals.map(
+ (x) =>
+ mangoMint && (
+
+ )
+ )
+ ) : (
+
+
+
+
{t('no-active-proposals')}
+
+
+ )}
+
+ >
+ )}
+
+ )
+}
+
+export default Vote
diff --git a/components/governance/Vote/VoteCountdown.tsx b/components/governance/Vote/VoteCountdown.tsx
new file mode 100644
index 00000000..acba7ac9
--- /dev/null
+++ b/components/governance/Vote/VoteCountdown.tsx
@@ -0,0 +1,115 @@
+import React, { useEffect, useState } from 'react'
+import { Governance, Proposal } from '@solana/spl-governance'
+import dayjs from 'dayjs'
+import { useTranslation } from 'next-i18next'
+
+interface CountdownState {
+ days: number
+ hours: number
+ minutes: number
+ seconds: number
+}
+
+const ZeroCountdown: CountdownState = {
+ days: 0,
+ hours: 0,
+ minutes: 0,
+ seconds: 0,
+}
+
+const isZeroCountdown = (state: CountdownState) =>
+ state.days === 0 &&
+ state.hours === 0 &&
+ state.minutes === 0 &&
+ state.seconds === 0
+
+export function VoteCountdown({
+ proposal,
+ governance,
+}: {
+ proposal: Proposal
+ governance: Governance
+}) {
+ const { t } = useTranslation(['governance'])
+
+ const [countdown, setCountdown] = useState(ZeroCountdown)
+
+ useEffect(() => {
+ if (proposal.isVoteFinalized()) {
+ setCountdown(ZeroCountdown)
+ return
+ }
+
+ const getTimeToVoteEnd = () => {
+ const now = dayjs().unix()
+
+ let timeToVoteEnd = proposal.isPreVotingState()
+ ? governance.config.maxVotingTime
+ : (proposal.votingAt?.toNumber() ?? 0) +
+ governance.config.maxVotingTime -
+ now
+
+ if (timeToVoteEnd <= 0) {
+ return ZeroCountdown
+ }
+
+ const days = Math.floor(timeToVoteEnd / 86400)
+ timeToVoteEnd -= days * 86400
+
+ const hours = Math.floor(timeToVoteEnd / 3600) % 24
+ timeToVoteEnd -= hours * 3600
+
+ const minutes = Math.floor(timeToVoteEnd / 60) % 60
+ timeToVoteEnd -= minutes * 60
+
+ const seconds = Math.floor(timeToVoteEnd % 60)
+
+ return { days, hours, minutes, seconds }
+ }
+
+ const updateCountdown = () => {
+ const newState = getTimeToVoteEnd()
+ setCountdown(newState)
+ }
+
+ const interval = setInterval(() => {
+ updateCountdown()
+ }, 1000)
+
+ updateCountdown()
+ return () => clearInterval(interval)
+ }, [proposal, governance])
+
+ return (
+ <>
+ {isZeroCountdown(countdown) ? (
+ {t('voting-ended')}
+ ) : (
+
+
{t('ends')}
+ {countdown && countdown.days > 0 && (
+ <>
+
+ {countdown.days}d
+
+
:
+ >
+ )}
+
{countdown.hours}h
+
:
+
+ {countdown.minutes}m
+
+ {!countdown.days && (
+ <>
+
:
+
+ {countdown.seconds}s
+
+ >
+ )}
+
+ )}
+ >
+ )
+}
diff --git a/components/governance/Vote/VotePage.tsx b/components/governance/Vote/VotePage.tsx
new file mode 100644
index 00000000..e802fde6
--- /dev/null
+++ b/components/governance/Vote/VotePage.tsx
@@ -0,0 +1,15 @@
+import dynamic from 'next/dynamic'
+import GovernancePageWrapper from '../GovernancePageWrapper'
+
+const Vote = dynamic(() => import('./Vote'))
+
+const VotePage = () => {
+ return (
+
+
+
+
+
+ )
+}
+export default VotePage
diff --git a/components/governance/Vote/VoteProgress.tsx b/components/governance/Vote/VoteProgress.tsx
new file mode 100644
index 00000000..93c655cf
--- /dev/null
+++ b/components/governance/Vote/VoteProgress.tsx
@@ -0,0 +1,95 @@
+import Tooltip from '@components/shared/Tooltip'
+import {
+ CheckCircleIcon,
+ InformationCircleIcon,
+} from '@heroicons/react/20/solid'
+import { Governance, ProgramAccount, Proposal } from '@solana/spl-governance'
+import { MintInfo } from '@solana/spl-token'
+import GovernanceStore from '@store/governanceStore'
+import { useTranslation } from 'next-i18next'
+import { getMintMaxVoteWeight } from 'utils/governance/proposals'
+import { fmtTokenAmount } from 'utils/governance/tools'
+
+type Props = {
+ governance: ProgramAccount
+ proposal: ProgramAccount
+ communityMint: MintInfo
+}
+
+const QuorumProgress = ({ governance, proposal, communityMint }: Props) => {
+ const { t } = useTranslation(['governance'])
+
+ const realm = GovernanceStore((s) => s.realm)
+
+ const voteThresholdPct =
+ governance.account.config.communityVoteThreshold.value || 0
+
+ const maxVoteWeight =
+ realm &&
+ getMintMaxVoteWeight(
+ communityMint,
+ realm.account.config.communityMintMaxVoteWeightSource
+ )
+
+ const minimumYesVotes =
+ fmtTokenAmount(maxVoteWeight!, communityMint.decimals) *
+ (voteThresholdPct / 100)
+
+ const yesVoteCount = fmtTokenAmount(
+ proposal.account.getYesVoteCount(),
+ communityMint.decimals
+ )
+
+ const rawYesVotesRequired = minimumYesVotes - yesVoteCount
+ const votesRequiredInRange = rawYesVotesRequired < 0 ? 0 : rawYesVotesRequired
+ const yesVoteProgress = votesRequiredInRange
+ ? 100 - (votesRequiredInRange / minimumYesVotes) * 100
+ : 100
+ const yesVotesRequired =
+ communityMint.decimals == 0
+ ? Math.ceil(votesRequiredInRange)
+ : votesRequiredInRange
+
+ return (
+
+
+
+
+
{t('approval-q')}
+
+
+
+
+ {typeof yesVoteProgress !== 'undefined' && yesVoteProgress < 100 ? (
+
{`${(
+ yesVotesRequired ?? 0
+ ).toLocaleString(undefined, {
+ maximumFractionDigits: 0,
+ })} ${(yesVoteProgress ?? 0) > 0 ? 'more' : ''} Yes vote${
+ (yesVotesRequired ?? 0) > 1 ? 's' : ''
+ } required`}
+ ) : (
+
+
+
+ {t('required-approval-achieved')}
+
+
+ )}
+
+
+
+
= 100 ? 'bg-th-up' : 'bg-th-fgd-2'
+ } flex rounded`}
+ >
+
+
+ )
+}
+
+export default QuorumProgress
diff --git a/components/governance/Vote/VoteResult.tsx b/components/governance/Vote/VoteResult.tsx
new file mode 100644
index 00000000..6e795878
--- /dev/null
+++ b/components/governance/Vote/VoteResult.tsx
@@ -0,0 +1,67 @@
+import { Proposal } from '@solana/spl-governance'
+import VoteResultsBar from './VoteResultBar'
+import { fmtTokenAmount } from 'utils/governance/tools'
+import { MintInfo } from '@solana/spl-token'
+import { useTranslation } from 'next-i18next'
+
+type VoteResultsProps = {
+ proposal: Proposal
+ communityMint: MintInfo
+}
+
+const VoteResults = ({ proposal, communityMint }: VoteResultsProps) => {
+ const { t } = useTranslation(['governance'])
+
+ const yesVoteCount = fmtTokenAmount(
+ proposal.getYesVoteCount(),
+ communityMint.decimals
+ )
+ const noVoteCount = fmtTokenAmount(
+ proposal.getNoVoteCount(),
+ communityMint.decimals
+ )
+ const totalVoteCount = yesVoteCount + noVoteCount
+ const getRelativeVoteCount = (voteCount: number) =>
+ totalVoteCount === 0 ? 0 : (voteCount / totalVoteCount) * 100
+ const relativeYesVotes = getRelativeVoteCount(yesVoteCount)
+ const relativeNoVotes = getRelativeVoteCount(noVoteCount)
+
+ return (
+
+ {proposal ? (
+
+
+
+
{t('yes-votes')}
+
+ {(yesVoteCount ?? 0).toLocaleString()}
+
+ {relativeYesVotes?.toFixed(1)}%
+
+
+
+
+
{t('no-votes')}
+
+ {(noVoteCount ?? 0).toLocaleString()}
+
+ {relativeNoVotes?.toFixed(1)}%
+
+
+
+
+
+
+ ) : (
+ <>
+
+ >
+ )}
+
+ )
+}
+
+export default VoteResults
diff --git a/components/governance/Vote/VoteResultBar.tsx b/components/governance/Vote/VoteResultBar.tsx
new file mode 100644
index 00000000..8fbede2f
--- /dev/null
+++ b/components/governance/Vote/VoteResultBar.tsx
@@ -0,0 +1,42 @@
+type VoteResultsBarProps = {
+ approveVotePercentage: number
+ denyVotePercentage: number
+}
+
+const VoteResultsBar = ({
+ approveVotePercentage = 0,
+ denyVotePercentage = 0,
+}: VoteResultsBarProps) => {
+ return (
+ <>
+
+
2 || approveVotePercentage < 0.01
+ ? approveVotePercentage
+ : 2
+ }%`,
+ }}
+ className={`flex rounded-l bg-th-up ${
+ denyVotePercentage < 0.01 && 'rounded'
+ }`}
+ >
+
2 || denyVotePercentage < 0.01
+ ? denyVotePercentage
+ : 2
+ }%`,
+ }}
+ className={`flex rounded-r bg-th-down ${
+ approveVotePercentage < 0.01 && 'rounded'
+ }`}
+ >
+
+ >
+ )
+}
+
+export default VoteResultsBar
diff --git a/components/mobile/BottomBar.tsx b/components/mobile/BottomBar.tsx
index 2d8566f5..845e6b01 100644
--- a/components/mobile/BottomBar.tsx
+++ b/components/mobile/BottomBar.tsx
@@ -146,6 +146,11 @@ const MoreMenuPanel = ({
path="/governance/listToken"
icon={}
/>
+ }
+ />
import('@components/governance/ListToken/ListTokenPage')
+)
export async function getStaticProps({ locale }: { locale: string }) {
return {
@@ -17,7 +21,7 @@ export async function getStaticProps({ locale }: { locale: string }) {
}
const Governance: NextPage = () => {
- return
+ return
}
export default Governance
diff --git a/pages/governance/vote.tsx b/pages/governance/vote.tsx
new file mode 100644
index 00000000..32fd7030
--- /dev/null
+++ b/pages/governance/vote.tsx
@@ -0,0 +1,25 @@
+import type { NextPage } from 'next'
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
+import dynamic from 'next/dynamic'
+
+const VotePage = dynamic(() => import('@components/governance/Vote/VotePage'))
+
+export async function getStaticProps({ locale }: { locale: string }) {
+ return {
+ props: {
+ ...(await serverSideTranslations(locale, [
+ 'governance',
+ 'search',
+ 'common',
+ 'onboarding',
+ 'profile',
+ ])),
+ },
+ }
+}
+
+const ListToken: NextPage = () => {
+ return
+}
+
+export default ListToken
diff --git a/public/locales/en/common.json b/public/locales/en/common.json
index e732f01c..1accc710 100644
--- a/public/locales/en/common.json
+++ b/public/locales/en/common.json
@@ -167,6 +167,7 @@
"wallet-disconnected": "Disconnected from wallet",
"withdraw": "Withdraw",
"withdraw-amount": "Withdraw Amount",
- "list-token": "List Token"
+ "list-token": "List Token",
+ "vote": "Vote"
}
\ No newline at end of file
diff --git a/public/locales/en/governance.json b/public/locales/en/governance.json
index 40f107d8..72795295 100644
--- a/public/locales/en/governance.json
+++ b/public/locales/en/governance.json
@@ -3,7 +3,7 @@
"cancel": "Cancel",
"connect-wallet": "Connect Wallet",
"on-boarding-title": "Looks like currently connected wallet doesn't have any MNGO deposited inside realms",
- "on-boarding-description": "Before you continue. Deposit {{amount}} MNGO into",
+ "on-boarding-description": "Before you continue. Deposit at least {{amount}} MNGO into",
"on-boarding-deposit-info": "Your MNGO will be locked for the duration of the proposal.",
"tokens-deposited": "Tokens Deposited",
"new-listing": "New Token Listing",
@@ -54,5 +54,21 @@
"market-name": "Market Name",
"proposal-title": "Proposal Title",
"proposal-des": "Proposal Description",
- "error-proposal-creation": "Error on proposal creation or no confirmation of transactions"
+ "error-proposal-creation": "Error on proposal creation or no confirmation of transactions",
+ "vote-yes": "Vote Yes",
+ "vote-no": "Vote No",
+ "relinquish-vote": "Relinquish Vote",
+ "voting-ended": "Voting ended",
+ "ends": "Ends",
+ "approval-q": "Approval Quorum",
+ "required-approval-achieved": "Required approval achieved",
+ "no-votes": "No Votes",
+ "yes-votes": "Yes Votes",
+ "quorum-description": "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.",
+ "active-proposals": "Active Proposals",
+ "no-active-proposals": "No active proposals",
+ "your-votes": "Your Votes:",
+ "yes": "Yes",
+ "no": "No",
+ "current-vote": "Current Vote:"
}
\ No newline at end of file
diff --git a/public/locales/es/common.json b/public/locales/es/common.json
index e732f01c..1accc710 100644
--- a/public/locales/es/common.json
+++ b/public/locales/es/common.json
@@ -167,6 +167,7 @@
"wallet-disconnected": "Disconnected from wallet",
"withdraw": "Withdraw",
"withdraw-amount": "Withdraw Amount",
- "list-token": "List Token"
+ "list-token": "List Token",
+ "vote": "Vote"
}
\ No newline at end of file
diff --git a/public/locales/es/governance.json b/public/locales/es/governance.json
index 40f107d8..72795295 100644
--- a/public/locales/es/governance.json
+++ b/public/locales/es/governance.json
@@ -3,7 +3,7 @@
"cancel": "Cancel",
"connect-wallet": "Connect Wallet",
"on-boarding-title": "Looks like currently connected wallet doesn't have any MNGO deposited inside realms",
- "on-boarding-description": "Before you continue. Deposit {{amount}} MNGO into",
+ "on-boarding-description": "Before you continue. Deposit at least {{amount}} MNGO into",
"on-boarding-deposit-info": "Your MNGO will be locked for the duration of the proposal.",
"tokens-deposited": "Tokens Deposited",
"new-listing": "New Token Listing",
@@ -54,5 +54,21 @@
"market-name": "Market Name",
"proposal-title": "Proposal Title",
"proposal-des": "Proposal Description",
- "error-proposal-creation": "Error on proposal creation or no confirmation of transactions"
+ "error-proposal-creation": "Error on proposal creation or no confirmation of transactions",
+ "vote-yes": "Vote Yes",
+ "vote-no": "Vote No",
+ "relinquish-vote": "Relinquish Vote",
+ "voting-ended": "Voting ended",
+ "ends": "Ends",
+ "approval-q": "Approval Quorum",
+ "required-approval-achieved": "Required approval achieved",
+ "no-votes": "No Votes",
+ "yes-votes": "Yes Votes",
+ "quorum-description": "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.",
+ "active-proposals": "Active Proposals",
+ "no-active-proposals": "No active proposals",
+ "your-votes": "Your Votes:",
+ "yes": "Yes",
+ "no": "No",
+ "current-vote": "Current Vote:"
}
\ No newline at end of file
diff --git a/public/locales/ru/common.json b/public/locales/ru/common.json
index e732f01c..1accc710 100644
--- a/public/locales/ru/common.json
+++ b/public/locales/ru/common.json
@@ -167,6 +167,7 @@
"wallet-disconnected": "Disconnected from wallet",
"withdraw": "Withdraw",
"withdraw-amount": "Withdraw Amount",
- "list-token": "List Token"
+ "list-token": "List Token",
+ "vote": "Vote"
}
\ No newline at end of file
diff --git a/public/locales/ru/governance.json b/public/locales/ru/governance.json
index 40f107d8..72795295 100644
--- a/public/locales/ru/governance.json
+++ b/public/locales/ru/governance.json
@@ -3,7 +3,7 @@
"cancel": "Cancel",
"connect-wallet": "Connect Wallet",
"on-boarding-title": "Looks like currently connected wallet doesn't have any MNGO deposited inside realms",
- "on-boarding-description": "Before you continue. Deposit {{amount}} MNGO into",
+ "on-boarding-description": "Before you continue. Deposit at least {{amount}} MNGO into",
"on-boarding-deposit-info": "Your MNGO will be locked for the duration of the proposal.",
"tokens-deposited": "Tokens Deposited",
"new-listing": "New Token Listing",
@@ -54,5 +54,21 @@
"market-name": "Market Name",
"proposal-title": "Proposal Title",
"proposal-des": "Proposal Description",
- "error-proposal-creation": "Error on proposal creation or no confirmation of transactions"
+ "error-proposal-creation": "Error on proposal creation or no confirmation of transactions",
+ "vote-yes": "Vote Yes",
+ "vote-no": "Vote No",
+ "relinquish-vote": "Relinquish Vote",
+ "voting-ended": "Voting ended",
+ "ends": "Ends",
+ "approval-q": "Approval Quorum",
+ "required-approval-achieved": "Required approval achieved",
+ "no-votes": "No Votes",
+ "yes-votes": "Yes Votes",
+ "quorum-description": "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.",
+ "active-proposals": "Active Proposals",
+ "no-active-proposals": "No active proposals",
+ "your-votes": "Your Votes:",
+ "yes": "Yes",
+ "no": "No",
+ "current-vote": "Current Vote:"
}
\ No newline at end of file
diff --git a/public/locales/zh/common.json b/public/locales/zh/common.json
index e732f01c..1accc710 100644
--- a/public/locales/zh/common.json
+++ b/public/locales/zh/common.json
@@ -167,6 +167,7 @@
"wallet-disconnected": "Disconnected from wallet",
"withdraw": "Withdraw",
"withdraw-amount": "Withdraw Amount",
- "list-token": "List Token"
+ "list-token": "List Token",
+ "vote": "Vote"
}
\ No newline at end of file
diff --git a/public/locales/zh/governance.json b/public/locales/zh/governance.json
index 40f107d8..72795295 100644
--- a/public/locales/zh/governance.json
+++ b/public/locales/zh/governance.json
@@ -3,7 +3,7 @@
"cancel": "Cancel",
"connect-wallet": "Connect Wallet",
"on-boarding-title": "Looks like currently connected wallet doesn't have any MNGO deposited inside realms",
- "on-boarding-description": "Before you continue. Deposit {{amount}} MNGO into",
+ "on-boarding-description": "Before you continue. Deposit at least {{amount}} MNGO into",
"on-boarding-deposit-info": "Your MNGO will be locked for the duration of the proposal.",
"tokens-deposited": "Tokens Deposited",
"new-listing": "New Token Listing",
@@ -54,5 +54,21 @@
"market-name": "Market Name",
"proposal-title": "Proposal Title",
"proposal-des": "Proposal Description",
- "error-proposal-creation": "Error on proposal creation or no confirmation of transactions"
+ "error-proposal-creation": "Error on proposal creation or no confirmation of transactions",
+ "vote-yes": "Vote Yes",
+ "vote-no": "Vote No",
+ "relinquish-vote": "Relinquish Vote",
+ "voting-ended": "Voting ended",
+ "ends": "Ends",
+ "approval-q": "Approval Quorum",
+ "required-approval-achieved": "Required approval achieved",
+ "no-votes": "No Votes",
+ "yes-votes": "Yes Votes",
+ "quorum-description": "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.",
+ "active-proposals": "Active Proposals",
+ "no-active-proposals": "No active proposals",
+ "your-votes": "Your Votes:",
+ "yes": "Yes",
+ "no": "No",
+ "current-vote": "Current Vote:"
}
\ No newline at end of file
diff --git a/public/locales/zh_tw/common.json b/public/locales/zh_tw/common.json
index d69975fd..801ab217 100644
--- a/public/locales/zh_tw/common.json
+++ b/public/locales/zh_tw/common.json
@@ -1,99 +1,4 @@
{
- "404-description": "或者從來不存在...",
- "404-heading": "此頁被清算了",
- "accept-terms": "接受條款",
- "accept-terms-desc": "繼續,即表示您接受Mango",
- "account": "帳戶",
- "account-balance": "帳戶餘額",
- "account-closed": "關戶成功👋",
- "account-name": "帳戶標籤",
- "account-name-desc": "以標籤來整理帳戶",
- "account-settings": "帳戶設定",
- "account-update-failed": "更新帳戶出錯",
- "account-update-success": "更新帳戶成功",
- "account-value": "帳戶價值",
- "accounts": "帳戶",
- "actions": "動作",
- "add-new-account": "開戶",
- "agree-and-continue": "同意並繼續",
- "all": "全部",
- "amount": "數量",
- "amount-owed": "欠款",
- "asset-liability-weight": "資產/債務權重",
- "asset-liability-weight-desc": "資產權重在賬戶健康計算中對質押品價值進行扣減。資產權重越低,資產對質押品的影響越小。債務權重恰恰相反(在健康計算中增加債務價值)。",
- "asset-weight": "資產權重",
- "asset-weight-desc": "資產權重在賬戶健康計算中對質押品價值進行扣減。資產權重越低,資產對質押品的影響越小。",
- "available": "可用",
- "available-balance": "可用餘額",
- "bal": "餘額",
- "balance": "餘額",
- "balances": "餘額",
- "borrow": "借貨",
- "borrow-amount": "借貸數量",
- "borrow-fee": "借貸費用",
- "borrow-funds": "進行借貸",
- "borrow-rate": "借貸APR",
- "buy": "買",
- "cancel": "取消",
- "chart-unavailable": "無法顯示圖表",
- "clear-all": "清除全部",
- "close": "關閉",
- "close-account": "關戶",
- "close-account-desc": "你確定嗎? 關戶就無法恢復",
- "closing-account": "正在關閉帳戶...",
- "collateral-value": "質押品價值",
- "connect": "連接",
- "connect-balances": "連接而查看資產餘額",
- "connect-helper": "連接來開始",
- "copy-address": "拷貝地址",
- "copy-address-success": "地址被拷貝: {{pk}}",
- "country-not-allowed": "你的國家{{country}}不允許使用Mango",
- "country-not-allowed-tooltip": "你正在使用MangoDAO提供的開源介面。由於監管的不確定性因此處於謀些地區的人的行動會受到限制。",
- "create-account": "開戶",
- "creating-account": "正在開戶...",
- "cumulative-interest-value": "總累積利息",
- "daily-volume": "24小時交易量",
- "date": "日期",
- "date-from": "從",
- "date-to": "至",
- "delegate": "委託",
- "delegate-account": "委託帳戶",
- "delegate-account-info": "帳戶委託給 {{address}}",
- "delegate-desc": "以Mango帳戶委託給別的錢包地址",
- "delegate-placeholder": "輸入受委錢包地執",
- "delete": "刪除",
- "deposit": "存款",
- "deposit-amount": "存款數量",
- "deposit-more-sol": "您的SOL錢包餘額太低。請多存入以支付交易",
- "deposit-rate": "存款APR",
- "disconnect": "斷開連接",
- "documentation": "文檔",
- "edit": "編輯",
- "edit-account": "編輯帳戶標籤",
- "edit-profile-image": "切換頭像",
- "explorer": "瀏覽器",
- "fee": "費用",
- "fees": "費用",
- "free-collateral": "可用的質押品",
- "funding": "資金費",
- "get-started": "開始",
- "governance": "治理",
- "health": "健康度",
- "health-impact": "健康影響",
- "health-tooltip": "此在您進行交易之前預測您賬戶的健康狀況。第一個值是您當前的帳戶健康狀況,第二個值是您的預測帳戶健康狀況。",
- "history": "紀錄",
- "insufficient-sol": "Solana需要0.04454 SOL租金才能創建Mango賬戶。您關閉帳戶時租金將被退還。",
- "interest-earned": "獲取利息",
- "interest-earned-paid": "獲取利息",
- "leaderboard": "排行榜",
- "learn": "學",
- "leverage": "槓桿",
- "liability-weight": "債務權重",
- "liquidity": "流動性",
- "list-token": "創造市場",
- "loading": "加載中",
- "loan-origination-fee": "借貸費用",
- "loan-origination-fee-tooltip": "執行借貸費用是{{fee}}。",
"mango": "Mango",
"mango-stats": "Mango統計",
"market": "市場",
@@ -167,5 +72,7 @@
"wallet-balance": "錢包餘額",
"wallet-disconnected": "已斷開錢包連接",
"withdraw": "取款",
- "withdraw-amount": "取款額"
-}
\ No newline at end of file
+ "withdraw-amount": "取款額",
+ "list-token": "列表代币",
+ "vote": "投票"
+}
diff --git a/public/locales/zh_tw/governance.json b/public/locales/zh_tw/governance.json
index 40f107d8..72795295 100644
--- a/public/locales/zh_tw/governance.json
+++ b/public/locales/zh_tw/governance.json
@@ -3,7 +3,7 @@
"cancel": "Cancel",
"connect-wallet": "Connect Wallet",
"on-boarding-title": "Looks like currently connected wallet doesn't have any MNGO deposited inside realms",
- "on-boarding-description": "Before you continue. Deposit {{amount}} MNGO into",
+ "on-boarding-description": "Before you continue. Deposit at least {{amount}} MNGO into",
"on-boarding-deposit-info": "Your MNGO will be locked for the duration of the proposal.",
"tokens-deposited": "Tokens Deposited",
"new-listing": "New Token Listing",
@@ -54,5 +54,21 @@
"market-name": "Market Name",
"proposal-title": "Proposal Title",
"proposal-des": "Proposal Description",
- "error-proposal-creation": "Error on proposal creation or no confirmation of transactions"
+ "error-proposal-creation": "Error on proposal creation or no confirmation of transactions",
+ "vote-yes": "Vote Yes",
+ "vote-no": "Vote No",
+ "relinquish-vote": "Relinquish Vote",
+ "voting-ended": "Voting ended",
+ "ends": "Ends",
+ "approval-q": "Approval Quorum",
+ "required-approval-achieved": "Required approval achieved",
+ "no-votes": "No Votes",
+ "yes-votes": "Yes Votes",
+ "quorum-description": "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.",
+ "active-proposals": "Active Proposals",
+ "no-active-proposals": "No active proposals",
+ "your-votes": "Your Votes:",
+ "yes": "Yes",
+ "no": "No",
+ "current-vote": "Current Vote:"
}
\ No newline at end of file
diff --git a/store/governanceStore.ts b/store/governanceStore.ts
index 65b0d393..05cd56c6 100644
--- a/store/governanceStore.ts
+++ b/store/governanceStore.ts
@@ -1,5 +1,7 @@
import { AnchorProvider, BN } from '@project-serum/anchor'
import {
+ getAllProposals,
+ getProposal,
getTokenOwnerRecord,
getTokenOwnerRecordAddress,
Governance,
@@ -17,8 +19,8 @@ import {
} from 'utils/governance/constants'
import { getDeposits } from 'utils/governance/fetch/deposits'
import {
+ accountsToPubkeyMap,
fetchGovernances,
- fetchProposals,
fetchRealm,
} from 'utils/governance/tools'
import { ConnectionContext, EndpointTypes } from 'utils/governance/types'
@@ -37,19 +39,21 @@ type IGovernanceStore = {
vsrClient: VsrClient | null
loadingRealm: boolean
loadingVoter: boolean
+ loadingProposals: boolean
voter: {
voteWeight: BN
- wallet: PublicKey
tokenOwnerRecord: ProgramAccount | undefined | null
}
set: (x: (x: IGovernanceStore) => void) => void
initConnection: (connection: Connection) => void
initRealm: (connectionContext: ConnectionContext) => void
- fetchVoterWeight: (
+ fetchVoter: (
wallet: PublicKey,
vsrClient: VsrClient,
connectionContext: ConnectionContext
) => void
+ resetVoter: () => void
+ updateProposals: (proposalPk: PublicKey) => void
}
const GovernanceStore = create((set, get) => ({
@@ -60,13 +64,13 @@ const GovernanceStore = create((set, get) => ({
vsrClient: null,
loadingRealm: false,
loadingVoter: false,
+ loadingProposals: false,
voter: {
voteWeight: new BN(0),
- wallet: PublicKey.default,
tokenOwnerRecord: null,
},
set: (fn) => set(produce(fn)),
- fetchVoterWeight: async (
+ fetchVoter: async (
wallet: PublicKey,
vsrClient: VsrClient,
connectionContext: ConnectionContext
@@ -99,11 +103,17 @@ const GovernanceStore = create((set, get) => ({
})
set((state) => {
state.voter.voteWeight = votingPower
- state.voter.wallet = wallet
state.voter.tokenOwnerRecord = tokenOwnerRecord
state.loadingVoter = false
})
},
+ resetVoter: () => {
+ const set = get().set
+ set((state) => {
+ state.voter.voteWeight = new BN(0)
+ state.voter.tokenOwnerRecord = null
+ })
+ },
initConnection: async (connection) => {
const set = get().set
const connectionContext = {
@@ -141,18 +151,40 @@ const GovernanceStore = create((set, get) => ({
realmId: MANGO_REALM_PK,
}),
])
- const proposals = await fetchProposals({
- connectionContext: connectionContext,
- programId: MANGO_GOVERNANCE_PROGRAM,
- governances: Object.keys(governances).map((x) => new PublicKey(x)),
- })
set((state) => {
+ state.loadingProposals = true
+ })
+ const proposals = await getAllProposals(
+ connectionContext.current,
+ MANGO_GOVERNANCE_PROGRAM,
+ MANGO_REALM_PK
+ )
+ const proposalsObj = accountsToPubkeyMap(proposals.flatMap((p) => p))
+ set((state) => {
+ state.loadingProposals = false
state.realm = realm
state.governances = governances
- state.proposals = proposals
+ state.proposals = proposalsObj
state.loadingRealm = false
})
},
+ updateProposals: async (proposalPk: PublicKey) => {
+ const state = get()
+ const set = get().set
+ set((state) => {
+ state.loadingProposals = true
+ })
+ const proposal = await getProposal(
+ state.connectionContext!.current!,
+ proposalPk
+ )
+ const newProposals = { ...state.proposals }
+ newProposals[proposal.pubkey.toBase58()] = proposal
+ set((state) => {
+ state.proposals = newProposals
+ state.loadingProposals = false
+ })
+ },
}))
export default GovernanceStore
diff --git a/utils/governance/fetch/deposits.ts b/utils/governance/fetch/deposits.ts
index c73030b9..40d74dd7 100644
--- a/utils/governance/fetch/deposits.ts
+++ b/utils/governance/fetch/deposits.ts
@@ -1,4 +1,4 @@
-import { MintInfo } from '@blockworks-foundation/mango-v4'
+import { MintInfo } from '@solana/spl-token'
import { BN, EventParser } from '@coral-xyz/anchor'
import { Connection, PublicKey, Transaction } from '@solana/web3.js'
import {
diff --git a/utils/governance/fetch/getProposals.ts b/utils/governance/fetch/getProposals.ts
deleted file mode 100644
index ff876100..00000000
--- a/utils/governance/fetch/getProposals.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-import { bs58 } from '@project-serum/anchor/dist/cjs/utils/bytes'
-import {
- deserializeBorsh,
- getGovernanceSchemaForAccount,
- GovernanceAccountType,
- ProgramAccount,
- Proposal,
-} from '@solana/spl-governance'
-import { PublicKey } from '@solana/web3.js'
-import { ConnectionContext } from '../types'
-
-export const getProposals = async (
- pubkeys: PublicKey[],
- connection: ConnectionContext,
- programId: PublicKey
-) => {
- const proposalsRaw = await fetch(connection.endpoint, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify([
- ...pubkeys.map((x) => {
- return getProposalsFilter(
- programId,
- connection,
- bs58.encode(Uint8Array.from([GovernanceAccountType.ProposalV1])),
- x
- )
- }),
- ...pubkeys.map((x) => {
- return getProposalsFilter(
- programId,
- connection,
- bs58.encode(Uint8Array.from([GovernanceAccountType.ProposalV2])),
- x
- )
- }),
- ]),
- })
-
- const accounts: ProgramAccount[] = []
- const proposalsData = await proposalsRaw.json()
-
- const rawAccounts = proposalsData
- ? // eslint-disable-next-line @typescript-eslint/no-explicit-any
- proposalsData.flatMap((x: any) => x.result)
- : []
- for (const rawAccount of rawAccounts) {
- try {
- const getSchema = getGovernanceSchemaForAccount
- const data = Buffer.from(rawAccount.account.data[0], 'base64')
- const accountType = data[0]
- const account: ProgramAccount = {
- pubkey: new PublicKey(rawAccount.pubkey),
- account: deserializeBorsh(getSchema(accountType), Proposal, data),
- owner: new PublicKey(rawAccount.account.owner),
- }
-
- accounts.push(account)
- } catch (ex) {
- console.info(`Can't deserialize @ ${rawAccount.pubkey}, ${ex}.`)
- }
- }
- const acc: ProgramAccount[][] = []
- const reducedAccounts = accounts.reduce((acc, current) => {
- const exsitingIdx = acc.findIndex((x) =>
- x.find(
- (x) =>
- x.account.governance.toBase58() ===
- current.account.governance.toBase58()
- )
- )
- if (exsitingIdx > -1) {
- acc[exsitingIdx].push(current)
- } else {
- acc.push([current])
- }
- return acc
- }, acc)
- return reducedAccounts
-}
-
-const getProposalsFilter = (
- programId: PublicKey,
- connection: ConnectionContext,
- memcmpBytes: string,
- pk: PublicKey
-) => {
- return {
- jsonrpc: '2.0',
- id: 1,
- method: 'getProgramAccounts',
- params: [
- programId.toBase58(),
- {
- commitment: connection.current.commitment,
- encoding: 'base64',
- filters: [
- {
- memcmp: {
- offset: 0, // number of bytes
- bytes: memcmpBytes, // base58 encoded string
- },
- },
- {
- memcmp: {
- offset: 1,
- bytes: pk.toBase58(),
- },
- },
- ],
- },
- ],
- }
-}
diff --git a/utils/governance/instructions/castVote.ts b/utils/governance/instructions/castVote.ts
new file mode 100644
index 00000000..eddb4b94
--- /dev/null
+++ b/utils/governance/instructions/castVote.ts
@@ -0,0 +1,127 @@
+import { Connection, Keypair, TransactionInstruction } from '@solana/web3.js'
+import {
+ ChatMessageBody,
+ getGovernanceProgramVersion,
+ GOVERNANCE_CHAT_PROGRAM_ID,
+ Proposal,
+ TokenOwnerRecord,
+ VoteChoice,
+ VoteKind,
+ withPostChatMessage,
+} from '@solana/spl-governance'
+import { ProgramAccount } from '@solana/spl-governance'
+
+import { Vote } from '@solana/spl-governance'
+
+import { withCastVote } from '@solana/spl-governance'
+import { VsrClient } from '../voteStakeRegistryClient'
+import { MANGO_GOVERNANCE_PROGRAM, MANGO_REALM_PK } from '../constants'
+import { updateVoterWeightRecord } from './updateVoteWeightRecord'
+import { WalletContextState } from '@solana/wallet-adapter-react'
+import { MangoClient } from '@blockworks-foundation/mango-v4'
+import { notify } from 'utils/notifications'
+
+export async function castVote(
+ connection: Connection,
+ wallet: WalletContextState,
+ proposal: ProgramAccount,
+ tokenOwnerRecord: ProgramAccount,
+ voteKind: VoteKind,
+ vsrClient: VsrClient,
+ mangoClient: MangoClient,
+ message?: ChatMessageBody | undefined
+) {
+ const signers: Keypair[] = []
+ const instructions: TransactionInstruction[] = []
+
+ const walletPubkey = wallet.publicKey!
+ const governanceAuthority = walletPubkey
+ const payer = walletPubkey
+ const programVersion = await getGovernanceProgramVersion(
+ connection,
+ MANGO_GOVERNANCE_PROGRAM
+ )
+
+ const { updateVoterWeightRecordIx, voterWeightPk } =
+ await updateVoterWeightRecord(vsrClient, walletPubkey)
+ instructions.push(updateVoterWeightRecordIx)
+
+ // 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 = proposal.account.governingTokenMint
+
+ await withCastVote(
+ instructions,
+ MANGO_GOVERNANCE_PROGRAM,
+ programVersion,
+ MANGO_REALM_PK,
+ proposal.account.governance,
+ proposal.pubkey,
+ proposal.account.tokenOwnerRecord,
+ tokenOwnerRecord.pubkey,
+ governanceAuthority,
+ tokenMint,
+ vote,
+ payer,
+ voterWeightPk
+ )
+
+ if (message) {
+ const { updateVoterWeightRecordIx, voterWeightPk } =
+ await updateVoterWeightRecord(vsrClient, walletPubkey)
+ instructions.push(updateVoterWeightRecordIx)
+
+ await withPostChatMessage(
+ instructions,
+ signers,
+ GOVERNANCE_CHAT_PROGRAM_ID,
+ MANGO_GOVERNANCE_PROGRAM,
+ MANGO_REALM_PK,
+ proposal.account.governance,
+ proposal.pubkey,
+ tokenOwnerRecord.pubkey,
+ governanceAuthority,
+ payer,
+ undefined,
+ message,
+ voterWeightPk
+ )
+ }
+
+ const tx = await mangoClient.sendAndConfirmTransaction(instructions)
+ notify({
+ title: 'Transaction confirmed',
+ type: 'success',
+ txid: tx,
+ noSound: true,
+ })
+}
diff --git a/utils/governance/instructions/createProposal.ts b/utils/governance/instructions/createProposal.ts
index b1a3e19b..3a3b1c08 100644
--- a/utils/governance/instructions/createProposal.ts
+++ b/utils/governance/instructions/createProposal.ts
@@ -4,7 +4,6 @@ import {
getSignatoryRecordAddress,
ProgramAccount,
serializeInstructionToBase64,
- SYSTEM_PROGRAM_ID,
TokenOwnerRecord,
VoteType,
WalletSigner,
@@ -22,12 +21,8 @@ import {
import { chunk } from 'lodash'
import { MANGO_MINT } from 'utils/constants'
import { MANGO_GOVERNANCE_PROGRAM, MANGO_REALM_PK } from '../constants'
-import { DEFAULT_VSR_ID, VsrClient } from '../voteStakeRegistryClient'
-import {
- getRegistrarPDA,
- getVoterPDA,
- getVoterWeightPDA,
-} from '../accounts/vsrAccounts'
+import { VsrClient } from '../voteStakeRegistryClient'
+import { updateVoterWeightRecord } from './updateVoteWeightRecord'
export const createProposal = async (
connection: Connection,
@@ -57,27 +52,8 @@ export const createProposal = async (
const options = ['Approve']
const useDenyOption = true
- //will run only if plugin is connected with realm
- const { registrar } = await getRegistrarPDA(
- MANGO_REALM_PK,
- new PublicKey(MANGO_MINT),
- DEFAULT_VSR_ID
- )
- const { voter } = await getVoterPDA(registrar, walletPk, DEFAULT_VSR_ID)
- const { voterWeightPk } = await getVoterWeightPDA(
- registrar,
- walletPk,
- DEFAULT_VSR_ID
- )
- const updateVoterWeightRecordIx = await client.program.methods
- .updateVoterWeightRecord()
- .accounts({
- registrar,
- voter,
- voterWeightRecord: voterWeightPk,
- systemProgram: SYSTEM_PROGRAM_ID,
- })
- .instruction()
+ const { updateVoterWeightRecordIx, voterWeightPk } =
+ await updateVoterWeightRecord(client, walletPk)
instructions.push(updateVoterWeightRecordIx)
const proposalAddress = await withCreateProposal(
diff --git a/utils/governance/instructions/relinquishVote.ts b/utils/governance/instructions/relinquishVote.ts
new file mode 100644
index 00000000..95a0ae42
--- /dev/null
+++ b/utils/governance/instructions/relinquishVote.ts
@@ -0,0 +1,52 @@
+import { Connection, PublicKey, TransactionInstruction } from '@solana/web3.js'
+import {
+ getGovernanceProgramVersion,
+ Proposal,
+ TokenOwnerRecord,
+ withRelinquishVote,
+} from '@solana/spl-governance'
+import { ProgramAccount } from '@solana/spl-governance'
+import { MANGO_GOVERNANCE_PROGRAM, MANGO_REALM_PK } from '../constants'
+import { WalletContextState } from '@solana/wallet-adapter-react'
+import { MangoClient } from '@blockworks-foundation/mango-v4'
+import { notify } from 'utils/notifications'
+
+export async function relinquishVote(
+ connection: Connection,
+ wallet: WalletContextState,
+ proposal: ProgramAccount,
+ tokenOwnerRecord: ProgramAccount,
+ mangoClient: MangoClient,
+ voteRecord: PublicKey
+) {
+ const instructions: TransactionInstruction[] = []
+ const governanceAuthority = wallet.publicKey!
+ const beneficiary = wallet.publicKey!
+
+ const programVersion = await getGovernanceProgramVersion(
+ connection,
+ MANGO_GOVERNANCE_PROGRAM
+ )
+
+ await withRelinquishVote(
+ instructions,
+ MANGO_GOVERNANCE_PROGRAM,
+ programVersion,
+ MANGO_REALM_PK,
+ proposal.account.governance,
+ proposal.pubkey,
+ tokenOwnerRecord.pubkey,
+ proposal.account.governingTokenMint,
+ voteRecord,
+ governanceAuthority,
+ beneficiary
+ )
+
+ const tx = await mangoClient.sendAndConfirmTransaction(instructions)
+ notify({
+ title: 'Transaction confirmed',
+ type: 'success',
+ txid: tx,
+ noSound: true,
+ })
+}
diff --git a/utils/governance/instructions/updateVoteWeightRecord.ts b/utils/governance/instructions/updateVoteWeightRecord.ts
new file mode 100644
index 00000000..12508e71
--- /dev/null
+++ b/utils/governance/instructions/updateVoteWeightRecord.ts
@@ -0,0 +1,36 @@
+import { PublicKey } from '@solana/web3.js'
+import { MANGO_MINT, MANGO_REALM_PK } from '../constants'
+import { DEFAULT_VSR_ID, VsrClient } from '../voteStakeRegistryClient'
+import {
+ getRegistrarPDA,
+ getVoterPDA,
+ getVoterWeightPDA,
+} from '../accounts/vsrAccounts'
+import { SYSTEM_PROGRAM_ID } from '@solana/spl-governance'
+
+export const updateVoterWeightRecord = async (
+ client: VsrClient,
+ walletPk: PublicKey
+) => {
+ const { registrar } = await getRegistrarPDA(
+ MANGO_REALM_PK,
+ new PublicKey(MANGO_MINT),
+ DEFAULT_VSR_ID
+ )
+ const { voter } = await getVoterPDA(registrar, walletPk, DEFAULT_VSR_ID)
+ const { voterWeightPk } = await getVoterWeightPDA(
+ registrar,
+ walletPk,
+ DEFAULT_VSR_ID
+ )
+ const updateVoterWeightRecordIx = await client!.program.methods
+ .updateVoterWeightRecord()
+ .accounts({
+ registrar,
+ voter,
+ voterWeightRecord: voterWeightPk,
+ systemProgram: SYSTEM_PROGRAM_ID,
+ })
+ .instruction()
+ return { updateVoterWeightRecordIx, voterWeightPk }
+}
diff --git a/utils/governance/proposals.ts b/utils/governance/proposals.ts
new file mode 100644
index 00000000..23251292
--- /dev/null
+++ b/utils/governance/proposals.ts
@@ -0,0 +1,67 @@
+import { BN } from '@project-serum/anchor'
+import {
+ Governance,
+ MintMaxVoteWeightSource,
+ MintMaxVoteWeightSourceType,
+ Proposal,
+ ProposalState,
+} from '@solana/spl-governance'
+import BigNumber from 'bignumber.js'
+import dayjs from 'dayjs'
+import { MintInfo } from '@solana/spl-token'
+
+export const isInCoolOffTime = (
+ proposal: Proposal | undefined,
+ governance: Governance | undefined
+) => {
+ const mainVotingEndedAt = proposal?.signingOffAt
+ ?.addn(governance?.config.maxVotingTime || 0)
+ .toNumber()
+
+ const votingCoolOffTime = governance?.config.votingCoolOffTime || 0
+ const canFinalizeAt = mainVotingEndedAt
+ ? mainVotingEndedAt + votingCoolOffTime
+ : mainVotingEndedAt
+
+ const endOfProposalAndCoolOffTime = canFinalizeAt
+ ? dayjs(1000 * canFinalizeAt!)
+ : undefined
+
+ const isInCoolOffTime = endOfProposalAndCoolOffTime
+ ? dayjs().isBefore(endOfProposalAndCoolOffTime) &&
+ mainVotingEndedAt &&
+ dayjs().isAfter(mainVotingEndedAt * 1000)
+ : undefined
+
+ return !!isInCoolOffTime && proposal!.state !== ProposalState.Defeated
+}
+
+/** Returns max VoteWeight for given mint and max source */
+export function getMintMaxVoteWeight(
+ mint: MintInfo,
+ maxVoteWeightSource: MintMaxVoteWeightSource
+) {
+ if (maxVoteWeightSource.type === MintMaxVoteWeightSourceType.SupplyFraction) {
+ const supplyFraction = maxVoteWeightSource.getSupplyFraction()
+
+ const maxVoteWeight = new BigNumber(supplyFraction.toString())
+ .multipliedBy(mint.supply.toString())
+ .shiftedBy(-MintMaxVoteWeightSource.SUPPLY_FRACTION_DECIMALS)
+
+ return new BN(maxVoteWeight.dp(0, BigNumber.ROUND_DOWN).toString())
+ } else {
+ // absolute value
+ return maxVoteWeightSource.value
+ }
+}
+
+export const calculatePct = (c = new BN(0), total?: BN) => {
+ if (total?.isZero()) {
+ return 0
+ }
+
+ return new BN(100)
+ .mul(c)
+ .div(total ?? new BN(1))
+ .toNumber()
+}
diff --git a/utils/governance/tools.ts b/utils/governance/tools.ts
index bb90a219..8e7e0e13 100644
--- a/utils/governance/tools.ts
+++ b/utils/governance/tools.ts
@@ -1,4 +1,3 @@
-import { MintInfo } from '@blockworks-foundation/mango-v4'
import {
getGovernanceAccounts,
getRealm,
@@ -7,10 +6,8 @@ import {
pubkeyFilter,
} from '@solana/spl-governance'
import { Connection, PublicKey } from '@solana/web3.js'
-import { getProposals } from './fetch/getProposals'
-import { ConnectionContext } from './types'
import { TokenProgramAccount } from './accounts/vsrAccounts'
-import { u64, MintLayout } from '@solana/spl-token'
+import { u64, MintLayout, MintInfo } from '@solana/spl-token'
import BN from 'bn.js'
export async function fetchRealm({
@@ -43,25 +40,6 @@ export async function fetchGovernances({
return governancesMap
}
-export async function fetchProposals({
- connectionContext,
- programId,
- governances,
-}: {
- connectionContext: ConnectionContext
- programId: PublicKey
- governances: PublicKey[]
-}) {
- const proposalsByGovernance = await getProposals(
- governances,
- connectionContext,
- programId
- )
-
- const proposals = accountsToPubkeyMap(proposalsByGovernance.flatMap((p) => p))
- return proposals
-}
-
export function accountsToPubkeyMap(accounts: ProgramAccount[]) {
return arrayToRecord(accounts, (a) => a.pubkey.toBase58())
}