Skip to content

Commit

Permalink
CreateNewProposalWithInsufficientPower (#2440)
Browse files Browse the repository at this point in the history
  • Loading branch information
ppsimatikas authored Oct 4, 2024
1 parent ffd1448 commit f2ba02c
Show file tree
Hide file tree
Showing 6 changed files with 214 additions and 78 deletions.
24 changes: 17 additions & 7 deletions components/MultiChoiceForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import { XCircleIcon } from '@heroicons/react/solid'
import useGovernanceAssets from '@hooks/useGovernanceAssets'
import Input from '@components/inputs/Input'
import GovernedAccountSelect from '../pages/dao/[symbol]/proposal/components/GovernedAccountSelect'
import { PublicKey } from '@solana/web3.js'
import { AccountType, AssetAccount } from '@utils/uiTypes/assets'
import { useLegacyVoterWeight } from '@hooks/queries/governancePower'
import { Governance, ProgramAccount } from '@solana/spl-governance'
import { useEffect } from 'react'

const MultiChoiceForm = ({
multiChoiceForm,
Expand All @@ -17,7 +18,7 @@ const MultiChoiceForm = ({
updateMultiFormErrors,
}: {
multiChoiceForm: {
governance: PublicKey | undefined
governance: ProgramAccount<Governance> | null
options: string[]
}
updateMultiChoiceForm: any
Expand All @@ -37,6 +38,17 @@ const MultiChoiceForm = ({
updateMultiChoiceForm({ ...multiChoiceForm, [propertyName]: value })
}

const governedAccounts = assetAccounts.filter((x) =>
ownVoterWeight?.canCreateProposal(x.governance.account.config)
)

useEffect(() => {
handleMultiForm({
value: governedAccounts.length ? governedAccounts[0].governance : null,
propertyName: 'governance'
})
}, [governedAccounts.length]);

const handleNotaButton = () => {
const options = [...multiChoiceForm.options]
options.push(nota)
Expand Down Expand Up @@ -78,20 +90,18 @@ const MultiChoiceForm = ({
<div className="mt-8 mb-8">
<GovernedAccountSelect
label="Which wallet’s rules should this proposal follow?"
governedAccounts={assetAccounts.filter((x) =>
ownVoterWeight?.canCreateProposal(x.governance.account.config)
)}
governedAccounts={governedAccounts}
onChange={(value: AssetAccount) => {
handleMultiForm({
value: value.governance.pubkey,
value: value.governance,
propertyName: 'governance',
})
}}
value={
governance
? assetAccounts.find(
(x) =>
x.governance.pubkey.equals(governance) &&
x.governance.pubkey.equals(governance.pubkey) &&
x.type === AccountType.SOL
)
: null
Expand Down
26 changes: 17 additions & 9 deletions hooks/queries/addresses/tokenOwnerRecord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,17 @@ import { useQuery } from '@tanstack/react-query'
import { useRealmQuery } from '../realm'
import { useSelectedDelegatorStore } from 'stores/useSelectedDelegatorStore'

export const useAddressQuery_CouncilTokenOwner = () => {
export const useAddressQuery_CouncilTokenOwnerByPK = (owner: PublicKey | undefined) => {
const realm = useRealmQuery().data?.result
return useAddressQuery_TokenOwnerRecord(
realm?.owner,
realm?.pubkey,
realm?.account.config.councilMint,
owner
)
}

export const useAddressQuery_CouncilTokenOwner = () => {
const wallet = useWalletOnePointOh()
const selectedCouncilDelegator = useSelectedDelegatorStore(
(s) => s.councilDelegator
Expand All @@ -18,16 +27,20 @@ export const useAddressQuery_CouncilTokenOwner = () => {
? selectedCouncilDelegator
: wallet?.publicKey ?? undefined

return useAddressQuery_CouncilTokenOwnerByPK(owner)
}

export const useAddressQuery_CommunityTokenOwnerByPK = (owner: PublicKey | undefined) => {
const realm = useRealmQuery().data?.result
return useAddressQuery_TokenOwnerRecord(
realm?.owner,
realm?.pubkey,
realm?.account.config.councilMint,
realm?.account.communityMint,
owner
)
}

export const useAddressQuery_CommunityTokenOwner = () => {
const realm = useRealmQuery().data?.result
const wallet = useWalletOnePointOh()
const selectedCommunityDelegator = useSelectedDelegatorStore(
(s) => s.communityDelegator
Expand All @@ -40,12 +53,7 @@ export const useAddressQuery_CommunityTokenOwner = () => {
: // I wanted to eliminate `null` as a possible type
wallet?.publicKey ?? undefined

return useAddressQuery_TokenOwnerRecord(
realm?.owner,
realm?.pubkey,
realm?.account.communityMint,
owner
)
return useAddressQuery_CommunityTokenOwnerByPK(owner)
}

export const useAddressQuery_TokenOwnerRecord = (
Expand Down
12 changes: 12 additions & 0 deletions hooks/queries/tokenOwnerRecord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import asFindable from '@utils/queries/asFindable'
import {
useAddressQuery_CommunityTokenOwner,
useAddressQuery_CouncilTokenOwner,
useAddressQuery_CommunityTokenOwnerByPK,
useAddressQuery_CouncilTokenOwnerByPK
} from './addresses/tokenOwnerRecord'
import { useRealmQuery } from './realm'
import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext'
Expand Down Expand Up @@ -284,3 +286,13 @@ export const useUserCouncilTokenOwnerRecord = () => {
const { data: tokenOwnerRecordPubkey } = useAddressQuery_CouncilTokenOwner()
return useTokenOwnerRecordByPubkeyQuery(tokenOwnerRecordPubkey)
}

export const useUserCommunityTokenOwnerRecordByPK = (pk: PublicKey | undefined) => {
const { data: tokenOwnerRecordPubkey } = useAddressQuery_CommunityTokenOwnerByPK(pk)
return useTokenOwnerRecordByPubkeyQuery(tokenOwnerRecordPubkey)
}

export const useUserCouncilTokenOwnerRecordByPK = (pk: PublicKey | undefined) => {
const { data: tokenOwnerRecordPubkey } = useAddressQuery_CouncilTokenOwnerByPK(pk)
return useTokenOwnerRecordByPubkeyQuery(tokenOwnerRecordPubkey)
}
168 changes: 154 additions & 14 deletions hooks/useCreateProposal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,41 @@ import useLegacyConnectionContext from './useLegacyConnectionContext'
import queryClient from './queries/queryClient'
import { proposalQueryKeys } from './queries/proposal'
import { createLUTProposal } from 'actions/createLUTproposal'
import { useLegacyVoterWeight } from './queries/governancePower'
import {useVotingClients} from "@hooks/useVotingClients";
import { useRealmVoterWeightPlugins } from '@hooks/useRealmVoterWeightPlugins'
import useRealm from '@hooks/useRealm'
import useWalletOnePointOh from '@hooks/useWalletOnePointOh'
import BN from 'bn.js'
import { BigNumber } from 'bignumber.js'
import { Governance, ProgramAccount } from '@solana/spl-governance'
import {
useTokenOwnerRecordsDelegatedToUser,
useUserCommunityTokenOwnerRecordByPK, useUserCouncilTokenOwnerRecordByPK
} from '@hooks/queries/tokenOwnerRecord'
import { useSelectedDelegatorStore } from '../stores/useSelectedDelegatorStore'
import { shortenAddress } from '@utils/address'

export default function useCreateProposal() {
const connection = useLegacyConnectionContext()
const realm = useRealmQuery().data?.result
const config = useRealmConfigQuery().data?.result
const mint = useRealmCommunityMintInfoQuery().data?.result
const councilMint = useRealmCouncilMintInfoQuery().data?.result
const { result: ownVoterWeight } = useLegacyVoterWeight()
// const { result: ownVoterWeight } = useLegacyVoterWeight()
const {
power: communityPower,
proposer: communityProposer,
} = useProposeAs('community')
const { data: communityProposerData } = useUserCommunityTokenOwnerRecordByPK(communityProposer)
const communityProposerTokenRecord = communityProposerData?.result

const {
power: councilPower,
proposer: councilProposer,
} = useProposeAs('council')

const { data: councilProposerData } = useUserCouncilTokenOwnerRecordByPK(councilProposer)
const councilProposerTokenRecord = councilProposerData?.result

const { getRpcContext } = useRpcContext()
const votingClients = useVotingClients();
Expand Down Expand Up @@ -52,21 +77,26 @@ export default function useCreateProposal() {
governance.pubkey
)
const minCouncilTokensToCreateProposal = selectedGovernance?.account.config.minCouncilTokensToCreateProposal
const councilPower = ownVoterWeight?.councilTokenRecord?.account.governingTokenDepositAmount
const minCommunityTokensToCreateProposal = selectedGovernance?.account.config.minCommunityTokensToCreateProposal

const ownTokenRecord =
minCouncilTokensToCreateProposal && councilPower && councilPower >= minCouncilTokensToCreateProposal ?
ownVoterWeight?.councilTokenRecord :
ownVoterWeight?.communityTokenRecord
const useCouncilPower = minCouncilTokensToCreateProposal && councilPower && councilPower.gte(minCouncilTokensToCreateProposal)
const ownTokenRecord =
useCouncilPower ?
councilProposerTokenRecord :
communityProposerTokenRecord

if (!ownTokenRecord) throw new Error('token owner record does not exist')
if (!selectedGovernance) throw new Error('governance not found')
if (!realm) throw new Error()

if (!useCouncilPower && communityPower && minCommunityTokensToCreateProposal && communityPower.lt(minCommunityTokensToCreateProposal)) {
throw new Error('Not enough voting power')
}

// this is somewhat confusing - the basic idea is:
// although a vote may be by community vote, the proposer may create it with their council token
// The choice of which token to use is made when the token record is selected
const proposeByCouncil = ownVoterWeight?.councilTokenRecord?.pubkey.toBase58() === (ownTokenRecord?.pubkey.toBase58() ?? "");
const proposeByCouncil = councilProposer?.toBase58() === (ownTokenRecord?.pubkey.toBase58() ?? "");
// now we can we identify whether we are using the community or council voting client (to decide which (if any) plugins to use)
const votingClient = votingClients(proposeByCouncil ? 'council' : 'community');

Expand Down Expand Up @@ -143,12 +173,17 @@ export default function useCreateProposal() {
)

const minCouncilTokensToCreateProposal = selectedGovernance?.account.config.minCouncilTokensToCreateProposal
const councilPower = ownVoterWeight?.councilTokenRecord?.account.governingTokenDepositAmount
const minCommunityTokensToCreateProposal = selectedGovernance?.account.config.minCommunityTokensToCreateProposal

const ownTokenRecord =
minCouncilTokensToCreateProposal && councilPower && councilPower >= minCouncilTokensToCreateProposal ?
ownVoterWeight?.councilTokenRecord :
ownVoterWeight?.communityTokenRecord
const useCouncilPower = minCouncilTokensToCreateProposal && councilPower && councilPower.gte(minCouncilTokensToCreateProposal)
const ownTokenRecord =
useCouncilPower ?
councilProposerTokenRecord :
communityProposerTokenRecord

if (!useCouncilPower && communityPower && minCommunityTokensToCreateProposal && communityPower.lt(minCommunityTokensToCreateProposal)) {
throw new Error('Not enough voting power')
}

if (!ownTokenRecord) throw new Error('token owner record does not exist')
if (!selectedGovernance) throw new Error('governance not found')
Expand All @@ -157,7 +192,7 @@ export default function useCreateProposal() {
// this is somewhat confusing - the basic idea is:
// although a vote may be by community vote, the proposer may create it with their council token
// The choice of which token to use is made when the token record is selected
const proposeByCouncil = ownVoterWeight?.councilTokenRecord?.pubkey.toBase58() === (ownTokenRecord?.pubkey.toBase58() ?? "");
const proposeByCouncil = councilProposer?.toBase58() === (ownTokenRecord?.pubkey.toBase58() ?? "");
// now we can we identify whether we are using the community or council voting client (to decide which (if any) plugins to use)
const votingClient = votingClients(proposeByCouncil ? 'council' : 'community');

Expand Down Expand Up @@ -202,3 +237,108 @@ export default function useCreateProposal() {

return { handleCreateProposal, propose, proposeMultiChoice }
}

const useProposeAs = (
role: 'community' | 'council',
) => {
const wallet = useWalletOnePointOh()
const { voterWeightForWallet, isReady } = useRealmVoterWeightPlugins(role)

const { councilDelegator, communityDelegator} = useSelectedDelegatorStore()
const { data: delegatesArray } = useTokenOwnerRecordsDelegatedToUser()


let proposer = councilDelegator
? councilDelegator
: communityDelegator
? communityDelegator
: wallet?.publicKey || undefined

let maxPower = proposer ? voterWeightForWallet(proposer)?.value : new BN(0)
// The user hasn't selected a specific delegator to perform actions as
// We will use the delegator with the maximum power, or the user's wallet
if (!councilDelegator && !communityDelegator && delegatesArray) {
for (const delegator of delegatesArray) {
const p = voterWeightForWallet(delegator.account.governingTokenOwner)?.value
if (p && maxPower && p.gt(maxPower)) {
maxPower = p
proposer = delegator.account.governingTokenOwner
}
}
}

return {
power: maxPower,
proposer,
isReady
}
}

export const useCanCreateProposal = (
governance?: ProgramAccount<Governance> | null
) => {
const wallet = useWalletOnePointOh()
const connected = !!wallet?.connected

const realm = useRealmQuery().data?.result

const {
power: communityPower,
proposer: communityProposer,
isReady: communityReady
} = useProposeAs('community')

const {
power: councilPower,
proposer: councilProposer,
isReady: councilReady
} = useProposeAs('council')

const power = communityPower || councilPower
const proposer = communityPower ? communityProposer : councilProposer
const isReady = communityReady && councilReady

const {
toManyCommunityOutstandingProposalsForUser,
toManyCouncilOutstandingProposalsForUse,
} = useRealm()


const minWeightToCreateProposal = (governance?.pubkey == realm?.account.communityMint ?
governance?.account.config.minCommunityTokensToCreateProposal :
governance?.account.config.minCouncilTokensToCreateProposal) || undefined

const hasEnoughVotingPower = power?.gt(minWeightToCreateProposal || new BN(1))

const canCreateProposal =
realm &&
hasEnoughVotingPower &&
!toManyCommunityOutstandingProposalsForUser &&
!toManyCouncilOutstandingProposalsForUse

const minWeightToCreateProposalS = minWeightToCreateProposal
? new BigNumber(minWeightToCreateProposal.toString()).toString()
: "1"

const error = !connected
? 'Connect your wallet to create new proposal'
: isReady && !communityPower && !councilPower
? 'There is no governance configuration to create a new proposal'
: !hasEnoughVotingPower
? `Please select only one account with at least ${minWeightToCreateProposalS} governance power to create a new proposal.`
: toManyCommunityOutstandingProposalsForUser
? 'Too many community outstanding proposals. You need to finalize them before creating a new one.'
: toManyCouncilOutstandingProposalsForUse
? 'Too many council outstanding proposals. You need to finalize them before creating a new one.'
: ''

const warning = proposer
? `Add a proposal as: ${shortenAddress(proposer.toString())}.`
: ''

return {
canCreateProposal,
error,
warning
}
}
Loading

0 comments on commit f2ba02c

Please sign in to comment.