governance active proposal voting (#117)
* governance wip * governance store + helpers wip * get voter weight * on boarding realms component * token list wip * pyth oracle get * adv fields * hidden adv fields * create proposal fcn + mangolana * register market wip * list market fixes wip * translations * register to register trustless + layout changes * fix layout * match switchboard oracle * loaders * validation * style components * change icon * copy update * translations keys * fix translation * fix warning create open book market * fix * fix transaltion * fix * fix * fix pr * fix onboarding translation * fix token owner record error in console * remove axios * generic vsr idl * fix pr * remove mangolana * small folder ref * voting wip * code fix * menu * fix * additional prop in onboarding component * fix translation * fix transaltion * voting fixes * add basic styling * fix merge * fix files * fix * fix loaders * fix * fix loading voteRecord * fix voter state without wallet * fix * translations * fix translation * dynamic imports * copy updates, show vote weight and current vote * fix --------- Co-authored-by: saml33 <slam.uke@gmail.com>
This commit is contained in:
parent
544b7077cc
commit
453b62fdd7
|
@ -15,6 +15,7 @@ import {
|
|||
BanknotesIcon,
|
||||
NewspaperIcon,
|
||||
PlusCircleIcon,
|
||||
ArchiveBoxArrowDownIcon,
|
||||
} from '@heroicons/react/20/solid'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
@ -151,6 +152,15 @@ const SideNav = ({ collapsed }: { collapsed: boolean }) => {
|
|||
hideIconBg
|
||||
showTooltip={false}
|
||||
/>
|
||||
<MenuItem
|
||||
active={pathname === '/governance/vote'}
|
||||
collapsed={false}
|
||||
icon={<ArchiveBoxArrowDownIcon className="h-5 w-5" />}
|
||||
title={t('common:vote')}
|
||||
pagePath="/governance/vote"
|
||||
hideIconBg
|
||||
showTooltip={false}
|
||||
/>
|
||||
<MenuItem
|
||||
collapsed={false}
|
||||
icon={<LightBulbIcon className="h-5 w-5" />}
|
||||
|
|
|
@ -3,7 +3,7 @@ import ListToken from './ListToken/ListToken'
|
|||
|
||||
const GovernancePage = () => {
|
||||
return (
|
||||
<div className="p-8 pb-20 md:pb-16 lg:p-10">
|
||||
<div className="py-8 px-4 pb-20 sm:px-6 md:pb-16 lg:p-10">
|
||||
<GovernancePageWrapper>
|
||||
<ListToken />
|
||||
</GovernancePageWrapper>
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import dynamic from 'next/dynamic'
|
||||
import GovernancePageWrapper from '../GovernancePageWrapper'
|
||||
|
||||
const ListToken = dynamic(() => import('./ListToken'))
|
||||
|
||||
const ListTokenPage = () => {
|
||||
return (
|
||||
<div className="p-8 pb-20 md:pb-16 lg:p-10">
|
||||
<GovernancePageWrapper>
|
||||
<ListToken />
|
||||
</GovernancePageWrapper>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default ListTokenPage
|
|
@ -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<Proposal>
|
||||
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<VoteKind | undefined>(undefined)
|
||||
|
||||
const [voteRecordAddress, setVoteRecordAddress] = useState<PublicKey | null>(
|
||||
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 ? (
|
||||
<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 md:mb-0">{proposal.account.descriptionLink}</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
|
|
@ -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<MintInfo | null>(null)
|
||||
const [votingProposals, setVotingProposals] = useState<
|
||||
ProgramAccount<Proposal>[]
|
||||
>([])
|
||||
|
||||
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 (
|
||||
<div>
|
||||
<div className="mb-4 flex flex-wrap items-end justify-between">
|
||||
<h1 className="mr-4">{t('active-proposals')}</h1>
|
||||
<p className="whitespace-no-wrap mb-0.5 mt-2">
|
||||
{t('your-votes')}{' '}
|
||||
<span className="font-mono text-th-fgd-2">
|
||||
{!loadingVoter
|
||||
? fmtTokenAmount(voter.voteWeight, MANGO_MINT_DECIMALS)
|
||||
: 0}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
{loadingProposals || loadingRealm ? (
|
||||
<div className="space-y-3">
|
||||
<SheenLoader className="flex flex-1">
|
||||
<div className={`h-56 w-full bg-th-bkg-2`} />
|
||||
</SheenLoader>
|
||||
<SheenLoader className="flex flex-1">
|
||||
<div className={`h-56 w-full bg-th-bkg-2`} />
|
||||
</SheenLoader>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{!loadingVoter ? (
|
||||
<OnBoarding minVotes={new BN(1000000)}></OnBoarding>
|
||||
) : null}
|
||||
<div className="space-y-3">
|
||||
{votingProposals.length ? (
|
||||
votingProposals.map(
|
||||
(x) =>
|
||||
mangoMint && (
|
||||
<ProposalCard
|
||||
key={x.pubkey.toBase58()}
|
||||
proposal={x}
|
||||
mangoMint={mangoMint}
|
||||
></ProposalCard>
|
||||
)
|
||||
)
|
||||
) : (
|
||||
<div className="flex h-56 items-center justify-center rounded-lg border border-th-bkg-3 p-6">
|
||||
<div className="flex flex-col items-center">
|
||||
<NoSymbolIcon className="mb-1 h-6 w-6" />
|
||||
<p>{t('no-active-proposals')}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Vote
|
|
@ -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) ? (
|
||||
<div className="text-fgd-3">{t('voting-ended')}</div>
|
||||
) : (
|
||||
<div className="text-fgd-1 flex w-40 items-center">
|
||||
<div className="text-fgd-3 mr-1">{t('ends')}</div>
|
||||
{countdown && countdown.days > 0 && (
|
||||
<>
|
||||
<div className="bg-bkg-1 rounded px-1 py-0.5">
|
||||
{countdown.days}d
|
||||
</div>
|
||||
<span className="text-fgd-3 mx-0.5 font-bold">:</span>
|
||||
</>
|
||||
)}
|
||||
<div className="bg-bkg-1 rounded px-1 py-0.5">{countdown.hours}h</div>
|
||||
<span className="text-fgd-3 mx-0.5 font-bold">:</span>
|
||||
<div className="bg-bkg-1 rounded px-1 py-0.5">
|
||||
{countdown.minutes}m
|
||||
</div>
|
||||
{!countdown.days && (
|
||||
<>
|
||||
<span className="text-fgd-3 mx-0.5 font-bold">:</span>
|
||||
<div className="bg-bkg-1 w-9 rounded px-1 py-0.5">
|
||||
{countdown.seconds}s
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import dynamic from 'next/dynamic'
|
||||
import GovernancePageWrapper from '../GovernancePageWrapper'
|
||||
|
||||
const Vote = dynamic(() => import('./Vote'))
|
||||
|
||||
const VotePage = () => {
|
||||
return (
|
||||
<div className="py-8 px-4 pb-20 sm:px-6 md:pb-16 lg:p-10">
|
||||
<GovernancePageWrapper>
|
||||
<Vote />
|
||||
</GovernancePageWrapper>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default VotePage
|
|
@ -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<Governance>
|
||||
proposal: ProgramAccount<Proposal>
|
||||
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 (
|
||||
<div className="w-full 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">{t('approval-q')}</p>
|
||||
<Tooltip content={t('quorum-description')}>
|
||||
<InformationCircleIcon className="text-fgd-2 h-5 w-5 cursor-help" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
{typeof yesVoteProgress !== 'undefined' && yesVoteProgress < 100 ? (
|
||||
<p className="text-fgd-1 mb-0 font-bold">{`${(
|
||||
yesVotesRequired ?? 0
|
||||
).toLocaleString(undefined, {
|
||||
maximumFractionDigits: 0,
|
||||
})} ${(yesVoteProgress ?? 0) > 0 ? 'more' : ''} Yes vote${
|
||||
(yesVotesRequired ?? 0) > 1 ? 's' : ''
|
||||
} required`}</p>
|
||||
) : (
|
||||
<div className="flex items-center">
|
||||
<CheckCircleIcon className="text-green mr-1.5 h-5 w-5 flex-shrink-0" />
|
||||
<p className="text-fgd-1 mb-0 font-bold">
|
||||
{t('required-approval-achieved')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2.5 flex h-2 w-full flex-grow rounded bg-th-bkg-4">
|
||||
<div
|
||||
style={{
|
||||
width: `${yesVoteProgress}%`,
|
||||
}}
|
||||
className={`${
|
||||
(yesVoteProgress ?? 0) >= 100 ? 'bg-th-up' : 'bg-th-fgd-2'
|
||||
} flex rounded`}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default QuorumProgress
|
|
@ -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 (
|
||||
<div className="flex w-full items-center space-x-4">
|
||||
{proposal ? (
|
||||
<div className={`w-full rounded-md`}>
|
||||
<div className="flex">
|
||||
<div className="w-1/2">
|
||||
<p>{t('yes-votes')}</p>
|
||||
<p className={`hero-text font-bold text-th-fgd-1`}>
|
||||
{(yesVoteCount ?? 0).toLocaleString()}
|
||||
<span className="ml-1 text-xs font-normal text-th-fgd-3">
|
||||
{relativeYesVotes?.toFixed(1)}%
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-1/2 text-right">
|
||||
<p>{t('no-votes')}</p>
|
||||
<p className={`hero-text font-bold text-th-fgd-1`}>
|
||||
{(noVoteCount ?? 0).toLocaleString()}
|
||||
<span className="ml-1 text-xs font-normal text-th-fgd-3">
|
||||
{relativeNoVotes?.toFixed(1)}%
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<VoteResultsBar
|
||||
approveVotePercentage={relativeYesVotes!}
|
||||
denyVotePercentage={relativeNoVotes!}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="h-12 w-full animate-pulse rounded bg-th-bkg-3" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default VoteResults
|
|
@ -0,0 +1,42 @@
|
|||
type VoteResultsBarProps = {
|
||||
approveVotePercentage: number
|
||||
denyVotePercentage: number
|
||||
}
|
||||
|
||||
const VoteResultsBar = ({
|
||||
approveVotePercentage = 0,
|
||||
denyVotePercentage = 0,
|
||||
}: VoteResultsBarProps) => {
|
||||
return (
|
||||
<>
|
||||
<div className="mt-2.5 flex h-2 w-full flex-grow rounded bg-th-bkg-4">
|
||||
<div
|
||||
style={{
|
||||
width: `${
|
||||
approveVotePercentage > 2 || approveVotePercentage < 0.01
|
||||
? approveVotePercentage
|
||||
: 2
|
||||
}%`,
|
||||
}}
|
||||
className={`flex rounded-l bg-th-up ${
|
||||
denyVotePercentage < 0.01 && 'rounded'
|
||||
}`}
|
||||
></div>
|
||||
<div
|
||||
style={{
|
||||
width: `${
|
||||
denyVotePercentage > 2 || denyVotePercentage < 0.01
|
||||
? denyVotePercentage
|
||||
: 2
|
||||
}%`,
|
||||
}}
|
||||
className={`flex rounded-r bg-th-down ${
|
||||
approveVotePercentage < 0.01 && 'rounded'
|
||||
}`}
|
||||
></div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default VoteResultsBar
|
|
@ -146,6 +146,11 @@ const MoreMenuPanel = ({
|
|||
path="/governance/listToken"
|
||||
icon={<PlusCircleIcon className="h-5 w-5" />}
|
||||
/>
|
||||
<MoreMenuItem
|
||||
title={t('common:vote')}
|
||||
path="/governance/vote"
|
||||
icon={<PlusCircleIcon className="h-5 w-5" />}
|
||||
/>
|
||||
<MoreMenuItem
|
||||
title={t('learn')}
|
||||
path="https://docs.mango.markets/"
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import GovernancePage from '@components/governance/GovernancePage'
|
||||
import type { NextPage } from 'next'
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
const ListTokenPage = dynamic(
|
||||
() => 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 <GovernancePage />
|
||||
return <ListTokenPage />
|
||||
}
|
||||
|
||||
export default Governance
|
||||
|
|
|
@ -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 <VotePage />
|
||||
}
|
||||
|
||||
export default ListToken
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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:"
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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:"
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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:"
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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:"
|
||||
}
|
|
@ -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": "取款額"
|
||||
}
|
||||
"withdraw-amount": "取款額",
|
||||
"list-token": "列表代币",
|
||||
"vote": "投票"
|
||||
}
|
||||
|
|
|
@ -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:"
|
||||
}
|
|
@ -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<TokenOwnerRecord> | 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<IGovernanceStore>((set, get) => ({
|
||||
|
@ -60,13 +64,13 @@ const GovernanceStore = create<IGovernanceStore>((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<IGovernanceStore>((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<IGovernanceStore>((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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<Proposal>[] = []
|
||||
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<Proposal> = {
|
||||
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<Proposal>[][] = []
|
||||
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(),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
|
@ -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<Proposal>,
|
||||
tokenOwnerRecord: ProgramAccount<TokenOwnerRecord>,
|
||||
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,
|
||||
})
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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<Proposal>,
|
||||
tokenOwnerRecord: ProgramAccount<TokenOwnerRecord>,
|
||||
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,
|
||||
})
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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<T>(accounts: ProgramAccount<T>[]) {
|
||||
return arrayToRecord(accounts, (a) => a.pubkey.toBase58())
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue