From fe995c1aca8f7e4f04e9a829ac8167a27662a252 Mon Sep 17 00:00:00 2001 From: agrippa kellum Date: Fri, 29 Sep 2023 18:18:01 -0400 Subject: [PATCH 01/12] delegator batch voting for vanilla --- actions/castVote.ts | 137 ++++++++++++++++++++++++++----- hooks/queries/governancePower.ts | 25 +++--- hooks/queries/proposal.ts | 12 ++- 3 files changed, 141 insertions(+), 33 deletions(-) diff --git a/actions/castVote.ts b/actions/castVote.ts index 7b339dd40c..bc0b3bf4ff 100644 --- a/actions/castVote.ts +++ b/actions/castVote.ts @@ -1,7 +1,7 @@ import { + Connection, Keypair, PublicKey, - Transaction, TransactionInstruction, } from '@solana/web3.js' import { @@ -29,11 +29,13 @@ import { SequenceType, txBatchesToInstructionSetWithSigners, } from '@utils/sendTransactions' -import { sendTransaction } from '@utils/send' import { calcCostOfNftVote, checkHasEnoughSolToVote } from '@tools/nftVoteCalc' import useNftProposalStore from 'NftVotePlugin/NftProposalStore' import { HeliumVsrClient } from 'HeliumVotePlugin/sdk/client' import { NftVoterClient } from '@utils/uiTypes/NftVoterClient' +import { fetchRealmByPubkey } from '@hooks/queries/realm' +import { fetchProposalByPubkeyQuery } from '@hooks/queries/proposal' +import { findPluginName } from '@hooks/queries/governancePower' const getVetoTokenMint = ( proposal: ProgramAccount, @@ -50,6 +52,65 @@ const getVetoTokenMint = ( return vetoTokenMint } +const DELEGATOR_BATCH_VOTE_SUPPORT_BY_PLUGIN: Record< + ReturnType, + boolean +> = { + vanilla: true, + VSR: false, + HeliumVSR: false, + gateway: false, + NFT: false, + unknown: false, +} + +const createDelegatorVote = async ({ + connection, + realmPk, + proposalPk, + tokenOwnerRecordPk, + userPk, + vote, +}: { + connection: Connection + realmPk: PublicKey + proposalPk: PublicKey + tokenOwnerRecordPk: PublicKey + userPk: PublicKey + vote: Vote +}) => { + // + const realm = (await fetchRealmByPubkey(connection, realmPk)).result + if (!realm) throw new Error() + const proposal = (await fetchProposalByPubkeyQuery(connection, proposalPk)) + .result + if (!proposal) throw new Error() + + const programVersion = await getGovernanceProgramVersion( + connection, + realm.owner + ) + + const castVoteIxs: TransactionInstruction[] = [] + await withCastVote( + castVoteIxs, + realm.owner, + programVersion, + realm.pubkey, + proposal.account.governance, + proposal.pubkey, + proposal.account.tokenOwnerRecord, + tokenOwnerRecordPk, + userPk, + proposal.account.governingTokenMint, + vote, + userPk + //plugin?.voterWeightPk, + //plugin?.maxVoterWeightRecord + ) + return castVoteIxs +} + export async function castVote( { connection, wallet, programId, walletPubkey }: RpcContext, realm: ProgramAccount, @@ -60,9 +121,9 @@ export async function castVote( votingPlugin?: VotingClient, runAfterConfirmation?: (() => void) | null, voteWeights?: number[], - _additionalTokenOwnerRecords?: [] + additionalTokenOwnerRecords?: [] ) { - const signers: Keypair[] = [] + const chatMessageSigners: Keypair[] = [] const createCastNftVoteTicketIxs: TransactionInstruction[] = [] const createPostMessageTicketIxs: TransactionInstruction[] = [] @@ -84,7 +145,6 @@ export async function castVote( tokenOwnerRecord, createCastNftVoteTicketIxs ) - console.log('PLUGIN IXS', pluginCastVoteIxs) const isMulti = proposal.account.voteType !== VoteType.SINGLE_CHOICE && @@ -156,6 +216,25 @@ export async function castVote( plugin?.maxVoterWeightRecord ) + const delegatorCastVoteAtoms = + additionalTokenOwnerRecords && + DELEGATOR_BATCH_VOTE_SUPPORT_BY_PLUGIN[ + findPluginName(votingPlugin?.client?.program.programId) + ] + ? await Promise.all( + additionalTokenOwnerRecords.map((tokenOwnerRecordPk) => + createDelegatorVote({ + connection, + realmPk: realm.pubkey, + proposalPk: proposal.pubkey, + tokenOwnerRecordPk, + userPk: walletPubkey, + vote, + }) + ) + ) + : [] + const pluginPostMessageIxs: TransactionInstruction[] = [] const postMessageIxs: TransactionInstruction[] = [] if (message) { @@ -168,7 +247,7 @@ export async function castVote( await withPostChatMessage( postMessageIxs, - signers, + chatMessageSigners, GOVERNANCE_CHAT_PROGRAM_ID, programId, realm.pubkey, @@ -187,20 +266,38 @@ export async function castVote( const isHeliumVoter = votingPlugin?.client instanceof HeliumVsrClient if (!isNftVoter && !isHeliumVoter) { - const transaction = new Transaction() - transaction.add( - ...[ - ...pluginCastVoteIxs, - ...castVoteIxs, - ...pluginPostMessageIxs, - ...postMessageIxs, - ] + const batch1 = [ + ...pluginCastVoteIxs, + ...castVoteIxs, + ...pluginPostMessageIxs, + ...postMessageIxs, + ] + // chunk size chosen conservatively. "Atoms" refers to atomic clusters of instructions (namely, updatevoterweight? + vote) + const delegatorBatches = chunks(delegatorCastVoteAtoms, 2).map((x) => + x.flat() ) + const actions = [batch1, ...delegatorBatches].map((ixs) => ({ + instructionsSet: ixs.map((ix) => ({ + transactionInstruction: ix, + signers: chatMessageSigners.filter((kp) => + ix.keys.find((key) => key.isSigner && key.pubkey.equals(kp.publicKey)) + ), + })), + sequenceType: SequenceType.Parallel, + })) - await sendTransaction({ transaction, wallet, connection, signers }) - if (runAfterConfirmation) { - runAfterConfirmation() - } + await sendTransactionsV3({ + connection, + wallet, + transactionInstructions: actions, + callbacks: { + afterAllTxConfirmed: () => { + if (runAfterConfirmation) { + runAfterConfirmation() + } + }, + }, + }) } // we need to chunk instructions @@ -220,7 +317,7 @@ export async function castVote( return { instructionsSet: txBatchesToInstructionSetWithSigners( txBatch, - message ? [[], signers] : [], + message ? [[], chatMessageSigners] : [], // seeing signer related bugs when posting chat? This is likely culprit batchIdx ), sequenceType: SequenceType.Sequential, @@ -277,7 +374,7 @@ export async function castVote( return { instructionsSet: txBatchesToInstructionSetWithSigners( txBatch, - message ? [[], signers] : [], + message ? [[], chatMessageSigners] : [], // seeing signer related bugs when posting chat? This is likely culprit batchIdx ), sequenceType: SequenceType.Sequential, diff --git a/hooks/queries/governancePower.ts b/hooks/queries/governancePower.ts index 90d7b90975..7409def682 100644 --- a/hooks/queries/governancePower.ts +++ b/hooks/queries/governancePower.ts @@ -92,6 +92,19 @@ export const getNftGovpower = async ( return power } +export const findPluginName = (programId: PublicKey | undefined) => + programId === undefined + ? ('vanilla' as const) + : VSR_PLUGIN_PKS.includes(programId.toString()) + ? ('VSR' as const) + : HELIUM_VSR_PLUGINS_PKS.includes(programId.toString()) + ? 'HeliumVSR' + : NFT_PLUGINS_PKS.includes(programId.toString()) + ? 'NFT' + : GATEWAY_PLUGINS_PKS.includes(programId.toString()) + ? 'gateway' + : 'unknown' + export const determineVotingPowerType = async ( connection: Connection, realmPk: PublicKey, @@ -106,17 +119,7 @@ export const determineVotingPowerType = async ( ? config.result?.account.communityTokenConfig.voterWeightAddin : config.result?.account.councilTokenConfig.voterWeightAddin - return programId === undefined - ? ('vanilla' as const) - : VSR_PLUGIN_PKS.includes(programId.toString()) - ? ('VSR' as const) - : HELIUM_VSR_PLUGINS_PKS.includes(programId.toString()) - ? 'HeliumVSR' - : NFT_PLUGINS_PKS.includes(programId.toString()) - ? 'NFT' - : GATEWAY_PLUGINS_PKS.includes(programId.toString()) - ? 'gateway' - : 'unknown' + return findPluginName(programId) } export const useGovernancePowerAsync = ( diff --git a/hooks/queries/proposal.ts b/hooks/queries/proposal.ts index 82e0b44717..b91cba89f3 100644 --- a/hooks/queries/proposal.ts +++ b/hooks/queries/proposal.ts @@ -1,4 +1,4 @@ -import { PublicKey } from '@solana/web3.js' +import { Connection, PublicKey } from '@solana/web3.js' import { useQuery } from '@tanstack/react-query' import asFindable from '@utils/queries/asFindable' import { @@ -27,6 +27,15 @@ export const proposalQueryKeys = { ], } +export const fetchProposalByPubkeyQuery = ( + connection: Connection, + pubkey: PublicKey +) => + queryClient.fetchQuery({ + queryKey: proposalQueryKeys.byPubkey(connection.rpcEndpoint, pubkey), + queryFn: () => asFindable(getProposal)(connection, pubkey), + }) + export const useProposalByPubkeyQuery = (pubkey: PublicKey | undefined) => { const connection = useLegacyConnectionContext() @@ -41,7 +50,6 @@ export const useProposalByPubkeyQuery = (pubkey: PublicKey | undefined) => { }, enabled, }) - return query } From 2deda76860d5ce7d5e46d4eb92575839c0766f75 Mon Sep 17 00:00:00 2001 From: agrippa kellum Date: Fri, 29 Sep 2023 18:21:44 -0400 Subject: [PATCH 02/12] . --- hooks/useSubmitVote.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/hooks/useSubmitVote.ts b/hooks/useSubmitVote.ts index 10a14f962e..612a957dc4 100644 --- a/hooks/useSubmitVote.ts +++ b/hooks/useSubmitVote.ts @@ -30,6 +30,7 @@ import { TransactionInstruction } from '@solana/web3.js' import useProgramVersion from './useProgramVersion' import useVotingTokenOwnerRecords from './useVotingTokenOwnerRecords' import { useMemo } from 'react' +import { useTokenOwnerRecordsDelegatedToUser } from './queries/tokenOwnerRecord' export const useSubmitVote = () => { const wallet = useWalletOnePointOh() @@ -49,6 +50,8 @@ export const useSubmitVote = () => { config?.account.communityTokenConfig.voterWeightAddin?.toBase58() ) + const delegators = useTokenOwnerRecordsDelegatedToUser() + const { error, loading, execute } = useAsyncCallback( async ({ vote, @@ -81,6 +84,11 @@ export const useSubmitVote = () => { voteRecordQueryKeys.all(connection.cluster) ) } + const relevantDelegators = delegators?.filter((x) => + x.account.governingTokenMint.equals( + voterTokenRecord.account.governingTokenMint + ) + ) try { await castVote( @@ -92,7 +100,8 @@ export const useSubmitVote = () => { msg, client, confirmationCallback, - voteWeights + voteWeights, + relevantDelegators ) queryClient.invalidateQueries({ queryKey: proposalQueryKeys.all(connection.current.rpcEndpoint), From 80efc356ef61d0cf797fa0d84e6376cc5d51564c Mon Sep 17 00:00:00 2001 From: agrippa kellum Date: Mon, 2 Oct 2023 01:22:55 -0400 Subject: [PATCH 03/12] no actual reason not to just show the number 0 --- components/GovernancePower/GovernancePowerCard.tsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/components/GovernancePower/GovernancePowerCard.tsx b/components/GovernancePower/GovernancePowerCard.tsx index f5d54b0d7f..795be27064 100644 --- a/components/GovernancePower/GovernancePowerCard.tsx +++ b/components/GovernancePower/GovernancePowerCard.tsx @@ -38,12 +38,6 @@ const GovernancePowerCard = () => { const bothLoading = communityPower.loading && councilPower.loading - const bothZero = - communityPower.result !== undefined && - councilPower.result !== undefined && - communityPower.result.isZero() && - councilPower.result.isZero() - const realmConfig = useRealmConfigQuery().data?.result return ( @@ -58,10 +52,6 @@ const GovernancePowerCard = () => {
- ) : bothZero ? ( -
- You do not have any governance power in this dao -
) : (
{realmConfig?.account.communityTokenConfig.tokenType === From b3a08dc70cd42fade3a787373bef965670286f2a Mon Sep 17 00:00:00 2001 From: agrippa kellum Date: Mon, 2 Oct 2023 01:29:10 -0400 Subject: [PATCH 04/12] retire dysfunctional cardinal namespace in delegator select --- components/SelectPrimaryDelegators.tsx | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/components/SelectPrimaryDelegators.tsx b/components/SelectPrimaryDelegators.tsx index d3af4a2bb5..1c926dd7c8 100644 --- a/components/SelectPrimaryDelegators.tsx +++ b/components/SelectPrimaryDelegators.tsx @@ -1,14 +1,13 @@ -import { DisplayAddress } from '@cardinal/namespaces-components' import Select from '@components/inputs/Select' import useWalletOnePointOh from '@hooks/useWalletOnePointOh' import { useTokenOwnerRecordsDelegatedToUser } from '@hooks/queries/tokenOwnerRecord' import { useSelectedDelegatorStore } from 'stores/useSelectedDelegatorStore' import { PublicKey } from '@solana/web3.js' -import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext' import { useRealmQuery } from '@hooks/queries/realm' import { useMemo } from 'react' import { ProgramAccount, TokenOwnerRecord } from '@solana/spl-governance' import { capitalize } from '@utils/helpers' +import { abbreviateAddress } from '@utils/formatting' const YOUR_WALLET_VALUE = 'Your wallet' @@ -104,7 +103,6 @@ function PrimaryDelegatorSelect({ kind: 'community' | 'council' tors: ProgramAccount[] }) { - const connection = useLegacyConnectionContext() return (
@@ -118,13 +116,7 @@ function PrimaryDelegatorSelect({ componentLabel={ selectedDelegator ? (
- + {abbreviateAddress(selectedDelegator)}
) : ( @@ -141,13 +133,7 @@ function PrimaryDelegatorSelect({ value={delegatedTor.account.governingTokenOwner.toBase58()} >
- + {abbreviateAddress(delegatedTor.account.governingTokenOwner)}
From 3c0d0653661382e2878749f9278d41151433ad65 Mon Sep 17 00:00:00 2001 From: agrippa kellum Date: Tue, 3 Oct 2023 18:02:48 -0400 Subject: [PATCH 05/12] mv constants --- actions/castVote.ts | 13 +------------ constants/flags.ts | 14 ++++++++++++++ constants/plugins.ts | 18 ++++++++++++++++-- 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/actions/castVote.ts b/actions/castVote.ts index bc0b3bf4ff..ce4ddc09a3 100644 --- a/actions/castVote.ts +++ b/actions/castVote.ts @@ -36,6 +36,7 @@ import { NftVoterClient } from '@utils/uiTypes/NftVoterClient' import { fetchRealmByPubkey } from '@hooks/queries/realm' import { fetchProposalByPubkeyQuery } from '@hooks/queries/proposal' import { findPluginName } from '@hooks/queries/governancePower' +import { DELEGATOR_BATCH_VOTE_SUPPORT_BY_PLUGIN } from '@constants/flags' const getVetoTokenMint = ( proposal: ProgramAccount, @@ -52,18 +53,6 @@ const getVetoTokenMint = ( return vetoTokenMint } -const DELEGATOR_BATCH_VOTE_SUPPORT_BY_PLUGIN: Record< - ReturnType, - boolean -> = { - vanilla: true, - VSR: false, - HeliumVSR: false, - gateway: false, - NFT: false, - unknown: false, -} - const createDelegatorVote = async ({ connection, realmPk, diff --git a/constants/flags.ts b/constants/flags.ts index 548a5d1b9c..a928eb4260 100644 --- a/constants/flags.ts +++ b/constants/flags.ts @@ -1,3 +1,17 @@ +import { findPluginName } from './plugins' + export const SUPPORT_CNFTS = true export const ON_NFT_VOTER_V2 = false export const SHOW_DELEGATORS_LIST = false + +export const DELEGATOR_BATCH_VOTE_SUPPORT_BY_PLUGIN: Record< + ReturnType, + boolean +> = { + vanilla: true, + VSR: false, + HeliumVSR: false, + gateway: false, + NFT: false, + unknown: false, +} diff --git a/constants/plugins.ts b/constants/plugins.ts index 7f7808a8a4..c18345efdf 100644 --- a/constants/plugins.ts +++ b/constants/plugins.ts @@ -1,4 +1,5 @@ -import * as heliumVsrSdk from '@helium/voter-stake-registry-sdk' +import { PROGRAM_ID as HELIUM_VSR_PROGRAM_ID } from '@helium/voter-stake-registry-sdk' +import { PublicKey } from '@solana/web3.js' import { DEFAULT_NFT_VOTER_PLUGIN } from '@tools/constants' export const VSR_PLUGIN_PKS: string[] = [ @@ -10,7 +11,7 @@ export const VSR_PLUGIN_PKS: string[] = [ ] export const HELIUM_VSR_PLUGINS_PKS: string[] = [ - heliumVsrSdk.PROGRAM_ID.toBase58(), + HELIUM_VSR_PROGRAM_ID.toBase58(), ] export const NFT_PLUGINS_PKS: string[] = [ @@ -23,3 +24,16 @@ export const GATEWAY_PLUGINS_PKS: string[] = [ 'Ggatr3wgDLySEwA2qEjt1oiw4BUzp5yMLJyz21919dq6', 'GgathUhdrCWRHowoRKACjgWhYHfxCEdBi5ViqYN6HVxk', // v2, supporting composition ] + +export const findPluginName = (programId: PublicKey | undefined) => + programId === undefined + ? ('vanilla' as const) + : VSR_PLUGIN_PKS.includes(programId.toString()) + ? ('VSR' as const) + : HELIUM_VSR_PLUGINS_PKS.includes(programId.toString()) + ? 'HeliumVSR' + : NFT_PLUGINS_PKS.includes(programId.toString()) + ? 'NFT' + : GATEWAY_PLUGINS_PKS.includes(programId.toString()) + ? 'gateway' + : 'unknown' From 97adfaa8587d1d0e65159faf3d30bf6404cdaf1e Mon Sep 17 00:00:00 2001 From: agrippa kellum Date: Tue, 3 Oct 2023 18:44:22 -0400 Subject: [PATCH 06/12] move useCanVote hook, and make it check for delegators --- components/VotePanel/CastMultiVoteButtons.tsx | 82 +++++++------- components/VotePanel/CastVoteButtons.tsx | 44 +------ components/VotePanel/useCanVote.ts | 107 ++++++++++++++++++ 3 files changed, 151 insertions(+), 82 deletions(-) create mode 100644 components/VotePanel/useCanVote.ts diff --git a/components/VotePanel/CastMultiVoteButtons.tsx b/components/VotePanel/CastMultiVoteButtons.tsx index 3d73af57fd..cdbbae63cb 100644 --- a/components/VotePanel/CastMultiVoteButtons.tsx +++ b/components/VotePanel/CastMultiVoteButtons.tsx @@ -1,19 +1,15 @@ import { Proposal, VoteKind } from '@solana/spl-governance' -import { CheckCircleIcon } from "@heroicons/react/solid"; +import { CheckCircleIcon } from '@heroicons/react/solid' import { useState } from 'react' import Button, { SecondaryButton } from '../Button' import VoteCommentModal from '../VoteCommentModal' -import { - useIsVoting, - useVoterTokenRecord, - useVotingPop, -} from './hooks' +import { useIsVoting, useVoterTokenRecord, useVotingPop } from './hooks' import { useProposalVoteRecordQuery } from '@hooks/queries/voteRecord' import { useSubmitVote } from '@hooks/useSubmitVote' import { useSelectedRealmInfo } from '@hooks/selectedRealm/useSelectedRealmRegistryEntry' -import { useCanVote } from './CastVoteButtons' +import { useCanVote } from './useCanVote' -export const CastMultiVoteButtons = ({proposal} : {proposal: Proposal}) => { +export const CastMultiVoteButtons = ({ proposal }: { proposal: Proposal }) => { const [showVoteModal, setShowVoteModal] = useState(false) const [vote, setVote] = useState<'yes' | 'no' | null>(null) const realmInfo = useSelectedRealmInfo() @@ -23,13 +19,15 @@ export const CastMultiVoteButtons = ({proposal} : {proposal: Proposal}) => { const voterTokenRecord = useVoterTokenRecord() const [canVote, tooltipContent] = useCanVote() const { data: ownVoteRecord } = useProposalVoteRecordQuery('electoral') - const [selectedOptions, setSelectedOptions] = useState([]); - const [optionStatus, setOptionStatus] = useState(new Array(proposal.options.length).fill(false)); + const [selectedOptions, setSelectedOptions] = useState([]) + const [optionStatus, setOptionStatus] = useState( + new Array(proposal.options.length).fill(false) + ) const isVoteCast = !!ownVoteRecord?.found const isVoting = useIsVoting() - const nota = "$$_NOTA_$$"; - const last = proposal.options.length - 1; + const nota = '$$_NOTA_$$' + const last = proposal.options.length - 1 const handleVote = async (vote: 'yes' | 'no') => { setVote(vote) @@ -40,50 +38,50 @@ export const CastMultiVoteButtons = ({proposal} : {proposal: Proposal}) => { await submitVote({ vote: vote === 'yes' ? VoteKind.Approve : VoteKind.Deny, voterTokenRecord: voterTokenRecord!, - voteWeights: selectedOptions + voteWeights: selectedOptions, }) } } const handleOption = (index: number) => { - let options = [...selectedOptions]; - let status = [...optionStatus]; - const isNota = proposal.options[last].label === nota; + let options = [...selectedOptions] + let status = [...optionStatus] + const isNota = proposal.options[last].label === nota - const selected = status[index]; + const selected = status[index] if (selected) { - options = options.filter(option => option !== index); + options = options.filter((option) => option !== index) status[index] = false } else { if (isNota) { if (index === last) { // if nota is clicked, unselect all other options - status = status.map(() => false); - status[index] = true; - options = [index]; + status = status.map(() => false) + status[index] = true + options = [index] } else { // remove nota from the selected if any other option is clicked - status[last] = false; - options = options.filter(option => option !== last); + status[last] = false + options = options.filter((option) => option !== last) if (!options.includes(index)) { options.push(index) } - status[index] = true; + status[index] = true } } else { if (!options.includes(index)) { options.push(index) } - status[index] = true; + status[index] = true } } - setSelectedOptions(options); - setOptionStatus(status); + setSelectedOptions(options) + setOptionStatus(status) } - return (isVoting && !isVoteCast) ? ( + return isVoting && !isVoteCast ? (

Cast your {votingPop} vote

@@ -99,29 +97,35 @@ export const CastMultiVoteButtons = ({proposal} : {proposal: Proposal}) => { handleOption(index)} disabled={!canVote || submitting} isLoading={submitting} > - {optionStatus[index] && } - {option.label === nota && index === last ? "None of the Above" : option.label} + {optionStatus[index] && ( + + )} + {option.label === nota && index === last + ? 'None of the Above' + : option.label}
- )} - )} + ) + })}
Note: You can select one or more options
) : null -} \ No newline at end of file +} diff --git a/components/VotePanel/CastVoteButtons.tsx b/components/VotePanel/CastVoteButtons.tsx index 5ade48dafb..5b27d1f4a7 100644 --- a/components/VotePanel/CastVoteButtons.tsx +++ b/components/VotePanel/CastVoteButtons.tsx @@ -9,52 +9,10 @@ import { useVoterTokenRecord, useVotingPop, } from './hooks' -import { VotingClientType } from '@utils/uiTypes/VotePlugin' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' -import useWalletOnePointOh from '@hooks/useWalletOnePointOh' import { useProposalVoteRecordQuery } from '@hooks/queries/voteRecord' import { useSubmitVote } from '@hooks/useSubmitVote' import { useSelectedRealmInfo } from '@hooks/selectedRealm/useSelectedRealmRegistryEntry' -import { useGovernancePowerAsync } from '@hooks/queries/governancePower' - -export const useCanVote = () => { - const client = useVotePluginsClientStore( - (s) => s.state.currentRealmVotingClient - ) - const votingPop = useVotingPop() - const { result: govPower } = useGovernancePowerAsync(votingPop) - const wallet = useWalletOnePointOh() - const connected = !!wallet?.connected - - const { data: ownVoteRecord } = useProposalVoteRecordQuery('electoral') - const voterTokenRecord = useVoterTokenRecord() - - const isVoteCast = !!ownVoteRecord?.found - - const hasMinAmountToVote = voterTokenRecord && govPower?.gtn(0) - - const canVote = - connected && - !( - client.clientType === VotingClientType.NftVoterClient && !voterTokenRecord - ) && - !( - client.clientType === VotingClientType.HeliumVsrClient && - !voterTokenRecord - ) && - !isVoteCast && - hasMinAmountToVote - - const voteTooltipContent = !connected - ? 'You need to connect your wallet to be able to vote' - : client.clientType === VotingClientType.NftVoterClient && !voterTokenRecord - ? 'You must join the Realm to be able to vote' - : !hasMinAmountToVote - ? 'You don’t have governance power to vote in this dao' - : '' - - return [canVote, voteTooltipContent] as const -} +import { useCanVote } from './useCanVote' export const CastVoteButtons = () => { const [showVoteModal, setShowVoteModal] = useState(false) diff --git a/components/VotePanel/useCanVote.ts b/components/VotePanel/useCanVote.ts new file mode 100644 index 0000000000..077491b323 --- /dev/null +++ b/components/VotePanel/useCanVote.ts @@ -0,0 +1,107 @@ +import { useVoterTokenRecord, useVotingPop } from './hooks' +import { VotingClientType } from '@utils/uiTypes/VotePlugin' +import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' +import useWalletOnePointOh from '@hooks/useWalletOnePointOh' +import { useProposalVoteRecordQuery } from '@hooks/queries/voteRecord' +import { + determineVotingPowerType, + useGovernancePowerAsync, +} from '@hooks/queries/governancePower' + +import { useConnection } from '@solana/wallet-adapter-react' +import { useAsync } from 'react-async-hook' +import { useSelectedDelegatorStore } from 'stores/useSelectedDelegatorStore' + +import { DELEGATOR_BATCH_VOTE_SUPPORT_BY_PLUGIN } from '@constants/flags' +import useSelectedRealmPubkey from '@hooks/selectedRealm/useSelectedRealmPubkey' +import { useRealmQuery } from '@hooks/queries/realm' +import { useTokenOwnerRecordsDelegatedToUser } from '@hooks/queries/tokenOwnerRecord' + +const useHasAnyVotingPower = (role: 'community' | 'council' | undefined) => { + const realmPk = useSelectedRealmPubkey() + const realm = useRealmQuery().data?.result + + const { connection } = useConnection() + + const relevantMint = + role && role === 'community' + ? realm?.account.communityMint + : realm?.account.config.councilMint + + const { result: personalAmount } = useGovernancePowerAsync(role) + + const { result: plugin } = useAsync( + async () => + role && realmPk && determineVotingPowerType(connection, realmPk, role), + [connection, realmPk, role] + ) + + // DELEGATOR VOTING --------------------------------------------------------------- + + const batchVoteSupported = + plugin && DELEGATOR_BATCH_VOTE_SUPPORT_BY_PLUGIN[plugin] + // If the user is selecting a specific delegator, we want to just use that and not count the other delegators + const selectedDelegator = useSelectedDelegatorStore((s) => + role === 'community' ? s.communityDelegator : s.councilDelegator + ) + const torsDelegatedToUser = useTokenOwnerRecordsDelegatedToUser() + const relevantDelegators = selectedDelegator + ? undefined + : relevantMint && + torsDelegatedToUser?.filter((x) => + x.account.governingTokenMint.equals(relevantMint) + ) + + //--------------------------------------------------------------------------------- + // notably, this is ignoring whether the delegators actually have voting power, but it's not a big deal + const canBatchVote = + relevantDelegators === undefined || batchVoteSupported === undefined + ? undefined + : batchVoteSupported && relevantDelegators?.length !== 0 + + // technically, if you have a TOR you can vote even if there's no power. But that doesnt seem user friendly. + const canPersonallyVote = + personalAmount === undefined ? undefined : personalAmount.isZero() === false + + const canVote = canBatchVote || canPersonallyVote + + return canVote +} + +export const useCanVote = () => { + const client = useVotePluginsClientStore( + (s) => s.state.currentRealmVotingClient + ) + const votingPop = useVotingPop() + const wallet = useWalletOnePointOh() + const connected = !!wallet?.connected + + const { data: ownVoteRecord } = useProposalVoteRecordQuery('electoral') + const voterTokenRecord = useVoterTokenRecord() + + const isVoteCast = !!ownVoteRecord?.found + + const hasMinAmountToVote = useHasAnyVotingPower(votingPop) + + const canVote = + connected && + !( + client.clientType === VotingClientType.NftVoterClient && !voterTokenRecord + ) && + !( + client.clientType === VotingClientType.HeliumVsrClient && + !voterTokenRecord + ) && + !isVoteCast && + hasMinAmountToVote + + const voteTooltipContent = !connected + ? 'You need to connect your wallet to be able to vote' + : client.clientType === VotingClientType.NftVoterClient && !voterTokenRecord + ? 'You must join the Realm to be able to vote' + : !hasMinAmountToVote + ? 'You don’t have governance power to vote in this dao' + : '' + + return [canVote, voteTooltipContent] as const +} From 78addb52dd3d0020977a5d4931d28355aeb999a9 Mon Sep 17 00:00:00 2001 From: agrippa Date: Tue, 3 Oct 2023 23:02:13 +0000 Subject: [PATCH 07/12] dont track vscode settings --- .gitignore | 2 ++ .vscode/settings.json | 12 ------------ 2 files changed, 2 insertions(+), 12 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore index 97a8637876..55713a92f9 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,5 @@ yarn-error.log* # Sentry .sentryclirc + +.vscode/settings.json \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 92a022a112..0000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "typescript.tsdk": "node_modules/typescript/lib", - "cSpell.words": [ - "Addin", - "blockworks", - "lamports", - "solana", - "VERCEL", - "WSOL" - ], - "editor.formatOnSave": true -} From f5dcefe343748d6d6e509a7bd240dd490fbfc8d4 Mon Sep 17 00:00:00 2001 From: agrippa Date: Tue, 3 Oct 2023 23:31:06 +0000 Subject: [PATCH 08/12] typo --- hooks/useSubmitVote.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/hooks/useSubmitVote.ts b/hooks/useSubmitVote.ts index 612a957dc4..0a739a5fda 100644 --- a/hooks/useSubmitVote.ts +++ b/hooks/useSubmitVote.ts @@ -84,11 +84,13 @@ export const useSubmitVote = () => { voteRecordQueryKeys.all(connection.cluster) ) } - const relevantDelegators = delegators?.filter((x) => - x.account.governingTokenMint.equals( - voterTokenRecord.account.governingTokenMint + const relevantDelegators = delegators + ?.filter((x) => + x.account.governingTokenMint.equals( + voterTokenRecord.account.governingTokenMint + ) ) - ) + .map((x) => x.pubkey) try { await castVote( From e116d013509039664036d1a9da12b672ee9cef59 Mon Sep 17 00:00:00 2001 From: agrippa Date: Tue, 3 Oct 2023 23:32:48 +0000 Subject: [PATCH 09/12] typo --- actions/castVote.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/castVote.ts b/actions/castVote.ts index ce4ddc09a3..06f6ea87e8 100644 --- a/actions/castVote.ts +++ b/actions/castVote.ts @@ -110,7 +110,7 @@ export async function castVote( votingPlugin?: VotingClient, runAfterConfirmation?: (() => void) | null, voteWeights?: number[], - additionalTokenOwnerRecords?: [] + additionalTokenOwnerRecords?: PublicKey[] ) { const chatMessageSigners: Keypair[] = [] From 02b49541c1868a678e6d13dd0f3ca3a9d5973753 Mon Sep 17 00:00:00 2001 From: agrippa Date: Wed, 4 Oct 2023 00:20:19 +0000 Subject: [PATCH 10/12] remove prop drilling --- components/VoteCommentModal.tsx | 31 +++++++------ components/VotePanel/CastMultiVoteButtons.tsx | 5 +-- components/VotePanel/CastVoteButtons.tsx | 10 +---- components/VotePanel/VetoButtons.tsx | 8 +--- hooks/useSubmitVote.ts | 44 ++++++++++++++----- 5 files changed, 50 insertions(+), 48 deletions(-) diff --git a/components/VoteCommentModal.tsx b/components/VoteCommentModal.tsx index 362a6ff4fb..e5ea1e1c9d 100644 --- a/components/VoteCommentModal.tsx +++ b/components/VoteCommentModal.tsx @@ -7,15 +7,12 @@ import Loading from './Loading' import Modal from './Modal' import Input from './inputs/Input' import Tooltip from './Tooltip' -import { TokenOwnerRecord } from '@solana/spl-governance' -import { ProgramAccount } from '@solana/spl-governance' import { useSubmitVote } from '@hooks/useSubmitVote' interface VoteCommentModalProps { onClose: () => void isOpen: boolean vote: VoteKind - voterTokenRecord: ProgramAccount isMulti?: number[] } @@ -30,7 +27,6 @@ const VoteCommentModal: FunctionComponent = ({ onClose, isOpen, vote, - voterTokenRecord, isMulti, }) => { const [comment, setComment] = useState('') @@ -41,9 +37,8 @@ const VoteCommentModal: FunctionComponent = ({ const handleSubmit = async () => { await submitVote({ vote, - voterTokenRecord, comment, - voteWeights: isMulti + voteWeights: isMulti, }) onClose() @@ -78,16 +73,20 @@ const VoteCommentModal: FunctionComponent = ({ onClick={handleSubmit} >
- {!submitting && - isMulti ? "" : - (vote === VoteKind.Approve ? ( - - ) : vote === VoteKind.Deny ? ( - - ) : ( - - ))} - {submitting ? : Vote {isMulti ? "" : voteString}} + {!submitting && isMulti ? ( + '' + ) : vote === VoteKind.Approve ? ( + + ) : vote === VoteKind.Deny ? ( + + ) : ( + + )} + {submitting ? ( + + ) : ( + Vote {isMulti ? '' : voteString} + )}
diff --git a/components/VotePanel/CastMultiVoteButtons.tsx b/components/VotePanel/CastMultiVoteButtons.tsx index cdbbae63cb..8fd6511780 100644 --- a/components/VotePanel/CastMultiVoteButtons.tsx +++ b/components/VotePanel/CastMultiVoteButtons.tsx @@ -3,7 +3,7 @@ import { CheckCircleIcon } from '@heroicons/react/solid' import { useState } from 'react' import Button, { SecondaryButton } from '../Button' import VoteCommentModal from '../VoteCommentModal' -import { useIsVoting, useVoterTokenRecord, useVotingPop } from './hooks' +import { useIsVoting, useVotingPop } from './hooks' import { useProposalVoteRecordQuery } from '@hooks/queries/voteRecord' import { useSubmitVote } from '@hooks/useSubmitVote' import { useSelectedRealmInfo } from '@hooks/selectedRealm/useSelectedRealmRegistryEntry' @@ -16,7 +16,6 @@ export const CastMultiVoteButtons = ({ proposal }: { proposal: Proposal }) => { const allowDiscussion = realmInfo?.allowDiscussion ?? true const { submitting, submitVote } = useSubmitVote() const votingPop = useVotingPop() - const voterTokenRecord = useVoterTokenRecord() const [canVote, tooltipContent] = useCanVote() const { data: ownVoteRecord } = useProposalVoteRecordQuery('electoral') const [selectedOptions, setSelectedOptions] = useState([]) @@ -37,7 +36,6 @@ export const CastMultiVoteButtons = ({ proposal }: { proposal: Proposal }) => { } else { await submitVote({ vote: vote === 'yes' ? VoteKind.Approve : VoteKind.Deny, - voterTokenRecord: voterTokenRecord!, voteWeights: selectedOptions, }) } @@ -144,7 +142,6 @@ export const CastMultiVoteButtons = ({ proposal }: { proposal: Proposal }) => { isOpen={showVoteModal} onClose={() => setShowVoteModal(false)} vote={VoteKind.Approve} - voterTokenRecord={voterTokenRecord!} isMulti={selectedOptions} /> ) : null} diff --git a/components/VotePanel/CastVoteButtons.tsx b/components/VotePanel/CastVoteButtons.tsx index 5b27d1f4a7..97b45d944e 100644 --- a/components/VotePanel/CastVoteButtons.tsx +++ b/components/VotePanel/CastVoteButtons.tsx @@ -3,12 +3,7 @@ import { useState } from 'react' import { ThumbUpIcon, ThumbDownIcon } from '@heroicons/react/solid' import Button from '../Button' import VoteCommentModal from '../VoteCommentModal' -import { - useIsInCoolOffTime, - useIsVoting, - useVoterTokenRecord, - useVotingPop, -} from './hooks' +import { useIsInCoolOffTime, useIsVoting, useVotingPop } from './hooks' import { useProposalVoteRecordQuery } from '@hooks/queries/voteRecord' import { useSubmitVote } from '@hooks/useSubmitVote' import { useSelectedRealmInfo } from '@hooks/selectedRealm/useSelectedRealmRegistryEntry' @@ -21,7 +16,6 @@ export const CastVoteButtons = () => { const allowDiscussion = realmInfo?.allowDiscussion ?? true const { submitting, submitVote } = useSubmitVote() const votingPop = useVotingPop() - const voterTokenRecord = useVoterTokenRecord() const [canVote, tooltipContent] = useCanVote() const { data: ownVoteRecord } = useProposalVoteRecordQuery('electoral') @@ -37,7 +31,6 @@ export const CastVoteButtons = () => { } else { await submitVote({ vote: vote === 'yes' ? VoteKind.Approve : VoteKind.Deny, - voterTokenRecord: voterTokenRecord!, }) } } @@ -89,7 +82,6 @@ export const CastVoteButtons = () => { isOpen={showVoteModal} onClose={() => setShowVoteModal(false)} vote={vote === 'yes' ? VoteKind.Approve : VoteKind.Deny} - voterTokenRecord={voterTokenRecord!} /> ) : null}
diff --git a/components/VotePanel/VetoButtons.tsx b/components/VotePanel/VetoButtons.tsx index 597f6c6683..334feabf40 100644 --- a/components/VotePanel/VetoButtons.tsx +++ b/components/VotePanel/VetoButtons.tsx @@ -71,7 +71,6 @@ const VetoButtons = () => { const vetoingPop = useVetoingPop() const canVeto = useCanVeto() const [openModal, setOpenModal] = useState(false) - const voterTokenRecord = useUserVetoTokenRecord() const { data: userVetoRecord } = useProposalVoteRecordQuery('veto') const { submitting, submitVote } = useSubmitVote() @@ -81,15 +80,11 @@ const VetoButtons = () => { } else { submitVote({ vote: VoteKind.Veto, - voterTokenRecord: voterTokenRecord!, }) } } - return vetoable && - vetoingPop && - voterTokenRecord && - !userVetoRecord?.found ? ( + return vetoable && vetoingPop && !userVetoRecord?.found ? ( <>
@@ -116,7 +111,6 @@ const VetoButtons = () => { setOpenModal(false)} isOpen={openModal} - voterTokenRecord={voterTokenRecord} vote={VoteKind.Veto} /> ) : null} diff --git a/hooks/useSubmitVote.ts b/hooks/useSubmitVote.ts index 0a739a5fda..230819cd84 100644 --- a/hooks/useSubmitVote.ts +++ b/hooks/useSubmitVote.ts @@ -9,10 +9,10 @@ import { ProgramAccount, Proposal, RpcContext, - TokenOwnerRecord, Vote, VoteChoice, VoteKind, + getTokenOwnerRecordAddress, withCastVote, } from '@solana/spl-governance' import { getProgramVersionForRealm } from '@models/registry/api' @@ -31,6 +31,7 @@ import useProgramVersion from './useProgramVersion' import useVotingTokenOwnerRecords from './useVotingTokenOwnerRecords' import { useMemo } from 'react' import { useTokenOwnerRecordsDelegatedToUser } from './queries/tokenOwnerRecord' +import useUserOrDelegator from './useUserOrDelegator' export const useSubmitVote = () => { const wallet = useWalletOnePointOh() @@ -50,22 +51,25 @@ export const useSubmitVote = () => { config?.account.communityTokenConfig.voterWeightAddin?.toBase58() ) + const actingAsWalletPk = useUserOrDelegator() const delegators = useTokenOwnerRecordsDelegatedToUser() const { error, loading, execute } = useAsyncCallback( async ({ vote, - voterTokenRecord, comment, voteWeights, }: { vote: VoteKind - voterTokenRecord: ProgramAccount comment?: string voteWeights?: number[] }) => { + if (!proposal) throw new Error() + if (!realm) throw new Error() + if (!actingAsWalletPk) throw new Error() + const rpcContext = new RpcContext( - proposal!.owner, + proposal.owner, getProgramVersionForRealm(realmInfo!), wallet!, connection.current, @@ -84,20 +88,36 @@ export const useSubmitVote = () => { voteRecordQueryKeys.all(connection.cluster) ) } + + const relevantMint = + vote !== VoteKind.Veto + ? // if its not a veto, business as usual + proposal.account.governingTokenMint + : // if it is a veto, the vetoing mint is the opposite of the governing mint + realm.account.communityMint.equals( + proposal.account.governingTokenMint + ) + ? realm.account.config.councilMint + : realm.account.communityMint + if (relevantMint === undefined) throw new Error() + + const tokenOwnerRecordPk = await getTokenOwnerRecordAddress( + realm.owner, + realm.pubkey, + relevantMint, + actingAsWalletPk + ) + const relevantDelegators = delegators - ?.filter((x) => - x.account.governingTokenMint.equals( - voterTokenRecord.account.governingTokenMint - ) - ) + ?.filter((x) => x.account.governingTokenMint.equals(relevantMint)) .map((x) => x.pubkey) try { await castVote( rpcContext, - realm!, - proposal!, - voterTokenRecord.pubkey, + realm, + proposal, + tokenOwnerRecordPk, vote, msg, client, From f0acefc535611a975dac667917996d75702f8f6d Mon Sep 17 00:00:00 2001 From: agrippa Date: Wed, 4 Oct 2023 01:14:31 +0000 Subject: [PATCH 11/12] vote without having any power of ur own --- actions/castVote.ts | 47 +++++++++++++++++++++++++ hooks/queries/useProgramVersionQuery.ts | 16 ++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/actions/castVote.ts b/actions/castVote.ts index 06f6ea87e8..9800a9c80c 100644 --- a/actions/castVote.ts +++ b/actions/castVote.ts @@ -15,6 +15,7 @@ import { VoteKind, VoteType, withPostChatMessage, + withCreateTokenOwnerRecord, } from '@solana/spl-governance' import { ProgramAccount } from '@solana/spl-governance' import { RpcContext } from '@solana/spl-governance' @@ -37,6 +38,8 @@ import { fetchRealmByPubkey } from '@hooks/queries/realm' import { fetchProposalByPubkeyQuery } from '@hooks/queries/proposal' import { findPluginName } from '@hooks/queries/governancePower' import { DELEGATOR_BATCH_VOTE_SUPPORT_BY_PLUGIN } from '@constants/flags' +import { fetchTokenOwnerRecordByPubkey } from '@hooks/queries/tokenOwnerRecord' +import { fetchProgramVersion } from '@hooks/queries/useProgramVersionQuery' const getVetoTokenMint = ( proposal: ProgramAccount, @@ -100,6 +103,42 @@ const createDelegatorVote = async ({ return castVoteIxs } +const createTokenOwnerRecordIfNeeded = async ({ + connection, + realmPk, + tokenOwnerRecordPk, + payer, + governingTokenMint, +}: { + connection: Connection + realmPk: PublicKey + tokenOwnerRecordPk: PublicKey + payer: PublicKey + governingTokenMint: PublicKey +}) => { + const realm = await fetchRealmByPubkey(connection, realmPk) + if (!realm.result) throw new Error() + const version = await fetchProgramVersion(connection, realm.result.owner) + + const tokenOwnerRecord = await fetchTokenOwnerRecordByPubkey( + connection, + tokenOwnerRecordPk + ) + if (tokenOwnerRecord.result) return [] + // create token owner record + const ixs: TransactionInstruction[] = [] + await withCreateTokenOwnerRecord( + ixs, + realm.result.owner, + version, + realmPk, + payer, + governingTokenMint, + payer + ) + return ixs +} + export async function castVote( { connection, wallet, programId, walletPubkey }: RpcContext, realm: ProgramAccount, @@ -253,9 +292,17 @@ export async function castVote( const isNftVoter = votingPlugin?.client instanceof NftVoterClient const isHeliumVoter = votingPlugin?.client instanceof HeliumVsrClient + const tokenOwnerRecordIxs = await createTokenOwnerRecordIfNeeded({ + connection, + realmPk: realm.pubkey, + tokenOwnerRecordPk: tokenOwnerRecord, + payer, + governingTokenMint: tokenMint, + }) if (!isNftVoter && !isHeliumVoter) { const batch1 = [ + ...tokenOwnerRecordIxs, ...pluginCastVoteIxs, ...castVoteIxs, ...pluginPostMessageIxs, diff --git a/hooks/queries/useProgramVersionQuery.ts b/hooks/queries/useProgramVersionQuery.ts index af7ecf25d1..4ac9581940 100644 --- a/hooks/queries/useProgramVersionQuery.ts +++ b/hooks/queries/useProgramVersionQuery.ts @@ -1,7 +1,8 @@ import { getGovernanceProgramVersion } from '@solana/spl-governance' import { useConnection } from '@solana/wallet-adapter-react' -import { PublicKey } from '@solana/web3.js' +import { Connection, PublicKey } from '@solana/web3.js' import { useQuery } from '@tanstack/react-query' +import queryClient from './queryClient' export const programVersionQueryKeys = { byProgramId: (endpoint: string, programId: PublicKey) => [ @@ -30,3 +31,16 @@ export function useProgramVersionByIdQuery( return query } + +export const fetchProgramVersion = ( + connection: Connection, + programId: PublicKey +) => + queryClient.fetchQuery({ + queryKey: programVersionQueryKeys.byProgramId( + connection.rpcEndpoint, + programId + ), + queryFn: () => getGovernanceProgramVersion(connection, programId), + staleTime: Number.MAX_SAFE_INTEGER, + }) From 7e0acc24eb8ecb31da69180fc50ee2e1c30923cd Mon Sep 17 00:00:00 2001 From: agrippa kellum Date: Wed, 18 Oct 2023 11:37:18 -0400 Subject: [PATCH 12/12] merge --- actions/castVote.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/actions/castVote.ts b/actions/castVote.ts index 16cb3f79d1..8e13367dbb 100644 --- a/actions/castVote.ts +++ b/actions/castVote.ts @@ -77,10 +77,7 @@ const createDelegatorVote = async ({ .result if (!proposal) throw new Error() - const programVersion = await getGovernanceProgramVersion( - connection, - realm.owner - ) + const programVersion = await fetchProgramVersion(connection, realm.owner) const castVoteIxs: TransactionInstruction[] = [] await withCastVote(