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 -} diff --git a/actions/castVote.ts b/actions/castVote.ts index 0dd54fbb2c..8e13367dbb 100644 --- a/actions/castVote.ts +++ b/actions/castVote.ts @@ -1,7 +1,7 @@ import { + Connection, Keypair, PublicKey, - Transaction, TransactionInstruction, } from '@solana/web3.js' import { @@ -14,6 +14,7 @@ import { VoteKind, VoteType, withPostChatMessage, + withCreateTokenOwnerRecord, } from '@solana/spl-governance' import { ProgramAccount } from '@solana/spl-governance' import { RpcContext } from '@solana/spl-governance' @@ -28,11 +29,15 @@ 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' +import { DELEGATOR_BATCH_VOTE_SUPPORT_BY_PLUGIN } from '@constants/flags' +import { fetchTokenOwnerRecordByPubkey } from '@hooks/queries/tokenOwnerRecord' import { fetchProgramVersion } from '@hooks/queries/useProgramVersionQuery' const getVetoTokenMint = ( @@ -50,6 +55,86 @@ const getVetoTokenMint = ( return vetoTokenMint } +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 fetchProgramVersion(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 +} + +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, @@ -60,9 +145,9 @@ export async function castVote( votingPlugin?: VotingClient, runAfterConfirmation?: (() => void) | null, voteWeights?: number[], - _additionalTokenOwnerRecords?: [] + additionalTokenOwnerRecords?: PublicKey[] ) { - const signers: Keypair[] = [] + const chatMessageSigners: Keypair[] = [] const createCastNftVoteTicketIxs: TransactionInstruction[] = [] const createPostMessageTicketIxs: TransactionInstruction[] = [] @@ -81,7 +166,6 @@ export async function castVote( tokenOwnerRecord, createCastNftVoteTicketIxs ) - console.log('PLUGIN IXS', pluginCastVoteIxs) const isMulti = proposal.account.voteType !== VoteType.SINGLE_CHOICE && @@ -153,6 +237,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) { @@ -165,7 +268,7 @@ export async function castVote( await withPostChatMessage( postMessageIxs, - signers, + chatMessageSigners, GOVERNANCE_CHAT_PROGRAM_ID, programId, realm.pubkey, @@ -182,22 +285,48 @@ 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 transaction = new Transaction() - transaction.add( - ...[ - ...pluginCastVoteIxs, - ...castVoteIxs, - ...pluginPostMessageIxs, - ...postMessageIxs, - ] + const batch1 = [ + ...tokenOwnerRecordIxs, + ...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 @@ -217,7 +346,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, @@ -274,7 +403,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/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 === 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)}
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 3d73af57fd..8fd6511780 100644 --- a/components/VotePanel/CastMultiVoteButtons.tsx +++ b/components/VotePanel/CastMultiVoteButtons.tsx @@ -1,35 +1,32 @@ 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, 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() 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([]); - 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) @@ -39,51 +36,50 @@ export const CastMultiVoteButtons = ({proposal} : {proposal: Proposal}) => { } else { 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 +95,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..97b45d944e 100644 --- a/components/VotePanel/CastVoteButtons.tsx +++ b/components/VotePanel/CastVoteButtons.tsx @@ -3,58 +3,11 @@ 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 { VotingClientType } from '@utils/uiTypes/VotePlugin' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' -import useWalletOnePointOh from '@hooks/useWalletOnePointOh' +import { useIsInCoolOffTime, useIsVoting, useVotingPop } from './hooks' 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) @@ -63,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') @@ -79,7 +31,6 @@ export const CastVoteButtons = () => { } else { await submitVote({ vote: vote === 'yes' ? VoteKind.Approve : VoteKind.Deny, - voterTokenRecord: voterTokenRecord!, }) } } @@ -131,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/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 +} 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' diff --git a/hooks/queries/governancePower.ts b/hooks/queries/governancePower.ts index 51cd17dbb2..f6c6a340a3 100644 --- a/hooks/queries/governancePower.ts +++ b/hooks/queries/governancePower.ts @@ -102,6 +102,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, @@ -116,17 +129,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 } diff --git a/hooks/useSubmitVote.ts b/hooks/useSubmitVote.ts index 10a14f962e..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' @@ -30,6 +30,8 @@ import { TransactionInstruction } from '@solana/web3.js' 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() @@ -49,20 +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, @@ -82,17 +89,41 @@ export const useSubmitVote = () => { ) } + 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(relevantMint)) + .map((x) => x.pubkey) + try { await castVote( rpcContext, - realm!, - proposal!, - voterTokenRecord.pubkey, + realm, + proposal, + tokenOwnerRecordPk, vote, msg, client, confirmationCallback, - voteWeights + voteWeights, + relevantDelegators ) queryClient.invalidateQueries({ queryKey: proposalQueryKeys.all(connection.current.rpcEndpoint),