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,
|
BanknotesIcon,
|
||||||
NewspaperIcon,
|
NewspaperIcon,
|
||||||
PlusCircleIcon,
|
PlusCircleIcon,
|
||||||
|
ArchiveBoxArrowDownIcon,
|
||||||
} from '@heroicons/react/20/solid'
|
} from '@heroicons/react/20/solid'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useTranslation } from 'next-i18next'
|
import { useTranslation } from 'next-i18next'
|
||||||
|
@ -151,6 +152,15 @@ const SideNav = ({ collapsed }: { collapsed: boolean }) => {
|
||||||
hideIconBg
|
hideIconBg
|
||||||
showTooltip={false}
|
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
|
<MenuItem
|
||||||
collapsed={false}
|
collapsed={false}
|
||||||
icon={<LightBulbIcon className="h-5 w-5" />}
|
icon={<LightBulbIcon className="h-5 w-5" />}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import ListToken from './ListToken/ListToken'
|
||||||
|
|
||||||
const GovernancePage = () => {
|
const GovernancePage = () => {
|
||||||
return (
|
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>
|
<GovernancePageWrapper>
|
||||||
<ListToken />
|
<ListToken />
|
||||||
</GovernancePageWrapper>
|
</GovernancePageWrapper>
|
||||||
|
|
|
@ -10,7 +10,8 @@ const GovernancePageWrapper = ({ children }: { children: ReactNode }) => {
|
||||||
const connectionContext = GovernanceStore((s) => s.connectionContext)
|
const connectionContext = GovernanceStore((s) => s.connectionContext)
|
||||||
const initRealm = GovernanceStore((s) => s.initRealm)
|
const initRealm = GovernanceStore((s) => s.initRealm)
|
||||||
const vsrClient = GovernanceStore((s) => s.vsrClient)
|
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 realm = GovernanceStore((s) => s.realm)
|
||||||
const connection = mangoStore((s) => s.connection)
|
const connection = mangoStore((s) => s.connection)
|
||||||
|
|
||||||
|
@ -32,7 +33,9 @@ const GovernancePageWrapper = ({ children }: { children: ReactNode }) => {
|
||||||
connectionContext?.endpoint &&
|
connectionContext?.endpoint &&
|
||||||
vsrClient?.program.programId.toBase58()
|
vsrClient?.program.programId.toBase58()
|
||||||
) {
|
) {
|
||||||
fetchVoterWeight(publicKey, vsrClient, connectionContext)
|
fetchVoter(publicKey, vsrClient, connectionContext)
|
||||||
|
} else {
|
||||||
|
resetVoter()
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
publicKey?.toBase58(),
|
publicKey?.toBase58(),
|
||||||
|
|
|
@ -89,7 +89,7 @@ const ListToken = () => {
|
||||||
const loadingRealm = GovernanceStore((s) => s.loadingRealm)
|
const loadingRealm = GovernanceStore((s) => s.loadingRealm)
|
||||||
const loadingVoter = GovernanceStore((s) => s.loadingVoter)
|
const loadingVoter = GovernanceStore((s) => s.loadingVoter)
|
||||||
const proposals = GovernanceStore((s) => s.proposals)
|
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 connectionContext = GovernanceStore((s) => s.connectionContext)
|
||||||
const { t } = useTranslation(['governance'])
|
const { t } = useTranslation(['governance'])
|
||||||
|
|
||||||
|
@ -364,7 +364,7 @@ const ListToken = () => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!wallet?.publicKey || !vsrClient || !connectionContext) 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) {
|
if (voter.voteWeight.cmp(minVoterWeight) === -1) {
|
||||||
notify({
|
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"
|
path="/governance/listToken"
|
||||||
icon={<PlusCircleIcon className="h-5 w-5" />}
|
icon={<PlusCircleIcon className="h-5 w-5" />}
|
||||||
/>
|
/>
|
||||||
|
<MoreMenuItem
|
||||||
|
title={t('common:vote')}
|
||||||
|
path="/governance/vote"
|
||||||
|
icon={<PlusCircleIcon className="h-5 w-5" />}
|
||||||
|
/>
|
||||||
<MoreMenuItem
|
<MoreMenuItem
|
||||||
title={t('learn')}
|
title={t('learn')}
|
||||||
path="https://docs.mango.markets/"
|
path="https://docs.mango.markets/"
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import GovernancePage from '@components/governance/GovernancePage'
|
|
||||||
import type { NextPage } from 'next'
|
import type { NextPage } from 'next'
|
||||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
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 }) {
|
export async function getStaticProps({ locale }: { locale: string }) {
|
||||||
return {
|
return {
|
||||||
|
@ -17,7 +21,7 @@ export async function getStaticProps({ locale }: { locale: string }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const Governance: NextPage = () => {
|
const Governance: NextPage = () => {
|
||||||
return <GovernancePage />
|
return <ListTokenPage />
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Governance
|
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",
|
"wallet-disconnected": "Disconnected from wallet",
|
||||||
"withdraw": "Withdraw",
|
"withdraw": "Withdraw",
|
||||||
"withdraw-amount": "Withdraw Amount",
|
"withdraw-amount": "Withdraw Amount",
|
||||||
"list-token": "List Token"
|
"list-token": "List Token",
|
||||||
|
"vote": "Vote"
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"connect-wallet": "Connect Wallet",
|
"connect-wallet": "Connect Wallet",
|
||||||
"on-boarding-title": "Looks like currently connected wallet doesn't have any MNGO deposited inside realms",
|
"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.",
|
"on-boarding-deposit-info": "Your MNGO will be locked for the duration of the proposal.",
|
||||||
"tokens-deposited": "Tokens Deposited",
|
"tokens-deposited": "Tokens Deposited",
|
||||||
"new-listing": "New Token Listing",
|
"new-listing": "New Token Listing",
|
||||||
|
@ -54,5 +54,21 @@
|
||||||
"market-name": "Market Name",
|
"market-name": "Market Name",
|
||||||
"proposal-title": "Proposal Title",
|
"proposal-title": "Proposal Title",
|
||||||
"proposal-des": "Proposal Description",
|
"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",
|
"wallet-disconnected": "Disconnected from wallet",
|
||||||
"withdraw": "Withdraw",
|
"withdraw": "Withdraw",
|
||||||
"withdraw-amount": "Withdraw Amount",
|
"withdraw-amount": "Withdraw Amount",
|
||||||
"list-token": "List Token"
|
"list-token": "List Token",
|
||||||
|
"vote": "Vote"
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"connect-wallet": "Connect Wallet",
|
"connect-wallet": "Connect Wallet",
|
||||||
"on-boarding-title": "Looks like currently connected wallet doesn't have any MNGO deposited inside realms",
|
"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.",
|
"on-boarding-deposit-info": "Your MNGO will be locked for the duration of the proposal.",
|
||||||
"tokens-deposited": "Tokens Deposited",
|
"tokens-deposited": "Tokens Deposited",
|
||||||
"new-listing": "New Token Listing",
|
"new-listing": "New Token Listing",
|
||||||
|
@ -54,5 +54,21 @@
|
||||||
"market-name": "Market Name",
|
"market-name": "Market Name",
|
||||||
"proposal-title": "Proposal Title",
|
"proposal-title": "Proposal Title",
|
||||||
"proposal-des": "Proposal Description",
|
"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",
|
"wallet-disconnected": "Disconnected from wallet",
|
||||||
"withdraw": "Withdraw",
|
"withdraw": "Withdraw",
|
||||||
"withdraw-amount": "Withdraw Amount",
|
"withdraw-amount": "Withdraw Amount",
|
||||||
"list-token": "List Token"
|
"list-token": "List Token",
|
||||||
|
"vote": "Vote"
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"connect-wallet": "Connect Wallet",
|
"connect-wallet": "Connect Wallet",
|
||||||
"on-boarding-title": "Looks like currently connected wallet doesn't have any MNGO deposited inside realms",
|
"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.",
|
"on-boarding-deposit-info": "Your MNGO will be locked for the duration of the proposal.",
|
||||||
"tokens-deposited": "Tokens Deposited",
|
"tokens-deposited": "Tokens Deposited",
|
||||||
"new-listing": "New Token Listing",
|
"new-listing": "New Token Listing",
|
||||||
|
@ -54,5 +54,21 @@
|
||||||
"market-name": "Market Name",
|
"market-name": "Market Name",
|
||||||
"proposal-title": "Proposal Title",
|
"proposal-title": "Proposal Title",
|
||||||
"proposal-des": "Proposal Description",
|
"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",
|
"wallet-disconnected": "Disconnected from wallet",
|
||||||
"withdraw": "Withdraw",
|
"withdraw": "Withdraw",
|
||||||
"withdraw-amount": "Withdraw Amount",
|
"withdraw-amount": "Withdraw Amount",
|
||||||
"list-token": "List Token"
|
"list-token": "List Token",
|
||||||
|
"vote": "Vote"
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"connect-wallet": "Connect Wallet",
|
"connect-wallet": "Connect Wallet",
|
||||||
"on-boarding-title": "Looks like currently connected wallet doesn't have any MNGO deposited inside realms",
|
"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.",
|
"on-boarding-deposit-info": "Your MNGO will be locked for the duration of the proposal.",
|
||||||
"tokens-deposited": "Tokens Deposited",
|
"tokens-deposited": "Tokens Deposited",
|
||||||
"new-listing": "New Token Listing",
|
"new-listing": "New Token Listing",
|
||||||
|
@ -54,5 +54,21 @@
|
||||||
"market-name": "Market Name",
|
"market-name": "Market Name",
|
||||||
"proposal-title": "Proposal Title",
|
"proposal-title": "Proposal Title",
|
||||||
"proposal-des": "Proposal Description",
|
"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": "Mango",
|
||||||
"mango-stats": "Mango統計",
|
"mango-stats": "Mango統計",
|
||||||
"market": "市場",
|
"market": "市場",
|
||||||
|
@ -167,5 +72,7 @@
|
||||||
"wallet-balance": "錢包餘額",
|
"wallet-balance": "錢包餘額",
|
||||||
"wallet-disconnected": "已斷開錢包連接",
|
"wallet-disconnected": "已斷開錢包連接",
|
||||||
"withdraw": "取款",
|
"withdraw": "取款",
|
||||||
"withdraw-amount": "取款額"
|
"withdraw-amount": "取款額",
|
||||||
|
"list-token": "列表代币",
|
||||||
|
"vote": "投票"
|
||||||
}
|
}
|
|
@ -3,7 +3,7 @@
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"connect-wallet": "Connect Wallet",
|
"connect-wallet": "Connect Wallet",
|
||||||
"on-boarding-title": "Looks like currently connected wallet doesn't have any MNGO deposited inside realms",
|
"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.",
|
"on-boarding-deposit-info": "Your MNGO will be locked for the duration of the proposal.",
|
||||||
"tokens-deposited": "Tokens Deposited",
|
"tokens-deposited": "Tokens Deposited",
|
||||||
"new-listing": "New Token Listing",
|
"new-listing": "New Token Listing",
|
||||||
|
@ -54,5 +54,21 @@
|
||||||
"market-name": "Market Name",
|
"market-name": "Market Name",
|
||||||
"proposal-title": "Proposal Title",
|
"proposal-title": "Proposal Title",
|
||||||
"proposal-des": "Proposal Description",
|
"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 { AnchorProvider, BN } from '@project-serum/anchor'
|
||||||
import {
|
import {
|
||||||
|
getAllProposals,
|
||||||
|
getProposal,
|
||||||
getTokenOwnerRecord,
|
getTokenOwnerRecord,
|
||||||
getTokenOwnerRecordAddress,
|
getTokenOwnerRecordAddress,
|
||||||
Governance,
|
Governance,
|
||||||
|
@ -17,8 +19,8 @@ import {
|
||||||
} from 'utils/governance/constants'
|
} from 'utils/governance/constants'
|
||||||
import { getDeposits } from 'utils/governance/fetch/deposits'
|
import { getDeposits } from 'utils/governance/fetch/deposits'
|
||||||
import {
|
import {
|
||||||
|
accountsToPubkeyMap,
|
||||||
fetchGovernances,
|
fetchGovernances,
|
||||||
fetchProposals,
|
|
||||||
fetchRealm,
|
fetchRealm,
|
||||||
} from 'utils/governance/tools'
|
} from 'utils/governance/tools'
|
||||||
import { ConnectionContext, EndpointTypes } from 'utils/governance/types'
|
import { ConnectionContext, EndpointTypes } from 'utils/governance/types'
|
||||||
|
@ -37,19 +39,21 @@ type IGovernanceStore = {
|
||||||
vsrClient: VsrClient | null
|
vsrClient: VsrClient | null
|
||||||
loadingRealm: boolean
|
loadingRealm: boolean
|
||||||
loadingVoter: boolean
|
loadingVoter: boolean
|
||||||
|
loadingProposals: boolean
|
||||||
voter: {
|
voter: {
|
||||||
voteWeight: BN
|
voteWeight: BN
|
||||||
wallet: PublicKey
|
|
||||||
tokenOwnerRecord: ProgramAccount<TokenOwnerRecord> | undefined | null
|
tokenOwnerRecord: ProgramAccount<TokenOwnerRecord> | undefined | null
|
||||||
}
|
}
|
||||||
set: (x: (x: IGovernanceStore) => void) => void
|
set: (x: (x: IGovernanceStore) => void) => void
|
||||||
initConnection: (connection: Connection) => void
|
initConnection: (connection: Connection) => void
|
||||||
initRealm: (connectionContext: ConnectionContext) => void
|
initRealm: (connectionContext: ConnectionContext) => void
|
||||||
fetchVoterWeight: (
|
fetchVoter: (
|
||||||
wallet: PublicKey,
|
wallet: PublicKey,
|
||||||
vsrClient: VsrClient,
|
vsrClient: VsrClient,
|
||||||
connectionContext: ConnectionContext
|
connectionContext: ConnectionContext
|
||||||
) => void
|
) => void
|
||||||
|
resetVoter: () => void
|
||||||
|
updateProposals: (proposalPk: PublicKey) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const GovernanceStore = create<IGovernanceStore>((set, get) => ({
|
const GovernanceStore = create<IGovernanceStore>((set, get) => ({
|
||||||
|
@ -60,13 +64,13 @@ const GovernanceStore = create<IGovernanceStore>((set, get) => ({
|
||||||
vsrClient: null,
|
vsrClient: null,
|
||||||
loadingRealm: false,
|
loadingRealm: false,
|
||||||
loadingVoter: false,
|
loadingVoter: false,
|
||||||
|
loadingProposals: false,
|
||||||
voter: {
|
voter: {
|
||||||
voteWeight: new BN(0),
|
voteWeight: new BN(0),
|
||||||
wallet: PublicKey.default,
|
|
||||||
tokenOwnerRecord: null,
|
tokenOwnerRecord: null,
|
||||||
},
|
},
|
||||||
set: (fn) => set(produce(fn)),
|
set: (fn) => set(produce(fn)),
|
||||||
fetchVoterWeight: async (
|
fetchVoter: async (
|
||||||
wallet: PublicKey,
|
wallet: PublicKey,
|
||||||
vsrClient: VsrClient,
|
vsrClient: VsrClient,
|
||||||
connectionContext: ConnectionContext
|
connectionContext: ConnectionContext
|
||||||
|
@ -99,11 +103,17 @@ const GovernanceStore = create<IGovernanceStore>((set, get) => ({
|
||||||
})
|
})
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.voter.voteWeight = votingPower
|
state.voter.voteWeight = votingPower
|
||||||
state.voter.wallet = wallet
|
|
||||||
state.voter.tokenOwnerRecord = tokenOwnerRecord
|
state.voter.tokenOwnerRecord = tokenOwnerRecord
|
||||||
state.loadingVoter = false
|
state.loadingVoter = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
resetVoter: () => {
|
||||||
|
const set = get().set
|
||||||
|
set((state) => {
|
||||||
|
state.voter.voteWeight = new BN(0)
|
||||||
|
state.voter.tokenOwnerRecord = null
|
||||||
|
})
|
||||||
|
},
|
||||||
initConnection: async (connection) => {
|
initConnection: async (connection) => {
|
||||||
const set = get().set
|
const set = get().set
|
||||||
const connectionContext = {
|
const connectionContext = {
|
||||||
|
@ -141,18 +151,40 @@ const GovernanceStore = create<IGovernanceStore>((set, get) => ({
|
||||||
realmId: MANGO_REALM_PK,
|
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) => {
|
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.realm = realm
|
||||||
state.governances = governances
|
state.governances = governances
|
||||||
state.proposals = proposals
|
state.proposals = proposalsObj
|
||||||
state.loadingRealm = false
|
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
|
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 { BN, EventParser } from '@coral-xyz/anchor'
|
||||||
import { Connection, PublicKey, Transaction } from '@solana/web3.js'
|
import { Connection, PublicKey, Transaction } from '@solana/web3.js'
|
||||||
import {
|
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,
|
getSignatoryRecordAddress,
|
||||||
ProgramAccount,
|
ProgramAccount,
|
||||||
serializeInstructionToBase64,
|
serializeInstructionToBase64,
|
||||||
SYSTEM_PROGRAM_ID,
|
|
||||||
TokenOwnerRecord,
|
TokenOwnerRecord,
|
||||||
VoteType,
|
VoteType,
|
||||||
WalletSigner,
|
WalletSigner,
|
||||||
|
@ -22,12 +21,8 @@ import {
|
||||||
import { chunk } from 'lodash'
|
import { chunk } from 'lodash'
|
||||||
import { MANGO_MINT } from 'utils/constants'
|
import { MANGO_MINT } from 'utils/constants'
|
||||||
import { MANGO_GOVERNANCE_PROGRAM, MANGO_REALM_PK } from '../constants'
|
import { MANGO_GOVERNANCE_PROGRAM, MANGO_REALM_PK } from '../constants'
|
||||||
import { DEFAULT_VSR_ID, VsrClient } from '../voteStakeRegistryClient'
|
import { VsrClient } from '../voteStakeRegistryClient'
|
||||||
import {
|
import { updateVoterWeightRecord } from './updateVoteWeightRecord'
|
||||||
getRegistrarPDA,
|
|
||||||
getVoterPDA,
|
|
||||||
getVoterWeightPDA,
|
|
||||||
} from '../accounts/vsrAccounts'
|
|
||||||
|
|
||||||
export const createProposal = async (
|
export const createProposal = async (
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
|
@ -57,27 +52,8 @@ export const createProposal = async (
|
||||||
const options = ['Approve']
|
const options = ['Approve']
|
||||||
const useDenyOption = true
|
const useDenyOption = true
|
||||||
|
|
||||||
//will run only if plugin is connected with realm
|
const { updateVoterWeightRecordIx, voterWeightPk } =
|
||||||
const { registrar } = await getRegistrarPDA(
|
await updateVoterWeightRecord(client, walletPk)
|
||||||
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()
|
|
||||||
instructions.push(updateVoterWeightRecordIx)
|
instructions.push(updateVoterWeightRecordIx)
|
||||||
|
|
||||||
const proposalAddress = await withCreateProposal(
|
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 {
|
import {
|
||||||
getGovernanceAccounts,
|
getGovernanceAccounts,
|
||||||
getRealm,
|
getRealm,
|
||||||
|
@ -7,10 +6,8 @@ import {
|
||||||
pubkeyFilter,
|
pubkeyFilter,
|
||||||
} from '@solana/spl-governance'
|
} from '@solana/spl-governance'
|
||||||
import { Connection, PublicKey } from '@solana/web3.js'
|
import { Connection, PublicKey } from '@solana/web3.js'
|
||||||
import { getProposals } from './fetch/getProposals'
|
|
||||||
import { ConnectionContext } from './types'
|
|
||||||
import { TokenProgramAccount } from './accounts/vsrAccounts'
|
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'
|
import BN from 'bn.js'
|
||||||
|
|
||||||
export async function fetchRealm({
|
export async function fetchRealm({
|
||||||
|
@ -43,25 +40,6 @@ export async function fetchGovernances({
|
||||||
return governancesMap
|
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>[]) {
|
export function accountsToPubkeyMap<T>(accounts: ProgramAccount<T>[]) {
|
||||||
return arrayToRecord(accounts, (a) => a.pubkey.toBase58())
|
return arrayToRecord(accounts, (a) => a.pubkey.toBase58())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue