Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CreateNewProposalWithInsufficientPower #2440

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -238,3 +240,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
Loading