Skip to content
This repository has been archived by the owner on Feb 9, 2025. It is now read-only.

Commit

Permalink
Merge pull request #1988 from solana-labs:agrippa/nft-batch-delegation
Browse files Browse the repository at this point in the history
nft delegation
  • Loading branch information
asktree authored Dec 15, 2023
2 parents 1ee1a4f + d4bda38 commit 085a374
Show file tree
Hide file tree
Showing 15 changed files with 317 additions and 257 deletions.
24 changes: 0 additions & 24 deletions NftVotePlugin/store/nftPluginStore.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,17 @@
import { MaxVoterWeightRecord, ProgramAccount } from '@solana/spl-governance'
import { VotingClient } from '@utils/uiTypes/VotePlugin'
import { DasNftObject } from '@hooks/queries/digitalAssets'
import create, { State } from 'zustand'

interface nftPluginStore extends State {
state: {
votingNfts: DasNftObject[]
maxVoteRecord: ProgramAccount<MaxVoterWeightRecord> | null
isLoadingNfts: boolean
}
setVotingNfts: (
nfts: DasNftObject[],
votingClient: VotingClient,
nftMintRegistrar: any
) => void
setMaxVoterWeight: (
maxVoterRecord: ProgramAccount<MaxVoterWeightRecord> | null
) => void
setIsLoadingNfts: (val: boolean) => void
}

const defaultState = {
votingNfts: [],
maxVoteRecord: null,
isLoadingNfts: false,
}

/**
Expand All @@ -34,18 +22,6 @@ const useNftPluginStore = create<nftPluginStore>((set, _get) => ({
state: {
...defaultState,
},
setIsLoadingNfts: (val) => {
set((s) => {
s.state.isLoadingNfts = val
})
},
setVotingNfts: (nfts, votingClient, _nftMintRegistrar) => {
votingClient._setCurrentVoterNfts(nfts)
set((s) => {
s.state.votingNfts = nfts
})
},

setMaxVoterWeight: (maxVoterRecord) => {
set((s) => {
s.state.maxVoteRecord = maxVoterRecord
Expand Down
9 changes: 6 additions & 3 deletions actions/castVote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ 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'
import { fetchVoteRecordByPubkey } from '@hooks/queries/voteRecord'
import { findPluginName } from '@constants/plugins'

const getVetoTokenMint = (
proposal: ProgramAccount<Proposal>,
Expand Down Expand Up @@ -447,14 +447,17 @@ export async function castVote(
}),
]
const totalVoteCost = await calcCostOfNftVote(
connection,
message,
instructionsChunks.length,
proposal.pubkey,
votingPlugin
votingPlugin,
realm.pubkey,
walletPubkey
)
const hasEnoughSol = await checkHasEnoughSolToVote(
totalVoteCost,
wallet.publicKey!,
walletPubkey,
connection
)
if (!hasEnoughSol) {
Expand Down
8 changes: 5 additions & 3 deletions components/NftVotingCountingModal.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useVotingNfts } from '@hooks/queries/plugins/nftVoter'
import { usePrevious } from '@hooks/usePrevious'
import useUserOrDelegator from '@hooks/useUserOrDelegator'
import useWalletOnePointOh from '@hooks/useWalletOnePointOh'
import { NftVoterClient } from '@utils/uiTypes/NftVoterClient'
import useNftProposalStore from 'NftVotePlugin/NftProposalStore'
import useNftPluginStore from 'NftVotePlugin/store/nftPluginStore'
import { useEffect, useState } from 'react'
import useTransactionsStore from 'stores/useTransactionStore'
import useVotePluginsClientStore from 'stores/useVotePluginsClientStore'
Expand All @@ -25,10 +26,11 @@ const NftVotingComponent = () => {
(s) => s.state.currentRealmVotingClient
)
const wallet = useWalletOnePointOh()
const { votingNfts } = useNftPluginStore((s) => s.state)
const userPk = useUserOrDelegator()
const votingNfts = useVotingNfts(userPk) ?? []
const votingInProgress = useNftProposalStore((s) => s.votingInProgress)
const usedNfts = countedNftsForProposal.length
const totalVotingPower = votingNfts.length
const totalVotingPower = votingNfts.length // TODO this is sometimes incorrect, power per nft is determined by config
const remainingNftsToCount = totalVotingPower - usedNfts
//in last tx there is max of 5 nfts
const lastTransactionNftsCount = 5
Expand Down
22 changes: 2 additions & 20 deletions components/ProposalVotingPower/LockedCommunityVotingPower.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import useRealm from '@hooks/useRealm'
import { BigNumber } from 'bignumber.js'
import { useMemo } from 'react'
import classNames from 'classnames'

import useDepositStore from 'VoteStakeRegistry/stores/useDepositStore'

import { getMintMetadata } from '../instructions/programs/splToken'
import { useRealmQuery } from '@hooks/queries/realm'
import { useTokenOwnerRecordsDelegatedToUser } from '@hooks/queries/tokenOwnerRecord'
import { useRealmCommunityMintInfoQuery } from '@hooks/queries/mintInfo'
import { useVsrGovpower } from '@hooks/queries/plugins/vsr'
import VSRCommunityVotingPower from 'VoteStakeRegistry/components/TokenBalance/VSRVotingPower'
import { useSelectedDelegatorStore } from 'stores/useSelectedDelegatorStore'
import DepositCommunityTokensBtn from 'VoteStakeRegistry/components/TokenBalance/DepositCommunityTokensBtn'
import useDelegators from '@components/VotePanel/useDelegators'

interface Props {
className?: string
Expand Down Expand Up @@ -40,24 +38,8 @@ export default function LockedCommunityVotingPower(props: Props) {
const tokenName =
getMintMetadata(depositMint)?.name ?? realm?.account.name ?? ''

const delegatedTors = useTokenOwnerRecordsDelegatedToUser()
const selectedDelegator = useSelectedDelegatorStore(
(s) => s.communityDelegator
)
// memoize useAsync inputs to prevent constant refetch
const relevantDelegators = useMemo(
() =>
selectedDelegator !== undefined // ignore delegators if any delegator is selected
? []
: delegatedTors
?.filter(
(x) =>
x.account.governingTokenMint.toString() ===
realm?.account.communityMint.toString()
)
.map((x) => x.account.governingTokenOwner),
[delegatedTors, realm?.account.communityMint, selectedDelegator]
)
const relevantDelegators = useDelegators('community')

if (isLoading || votingPowerLoading || !(votingPower && mint)) {
return (
Expand Down
77 changes: 49 additions & 28 deletions components/ProposalVotingPower/NftVotingPower.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
import { NftVoterClient } from '@utils/uiTypes/NftVoterClient'

import useNftPluginStore from 'NftVotePlugin/store/nftPluginStore'
import useRealm from '@hooks/useRealm'
import Button from '@components/Button'
import { getVoterWeightRecord } from '@utils/plugin/accounts'
import useVotePluginsClientStore from 'stores/useVotePluginsClientStore'
Expand All @@ -19,40 +18,35 @@ import VotingPowerPct from './VotingPowerPct'
import useWalletOnePointOh from '@hooks/useWalletOnePointOh'
import { useUserCommunityTokenOwnerRecord } from '@hooks/queries/tokenOwnerRecord'
import { useRealmQuery } from '@hooks/queries/realm'
import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext'
import { useGovernancePowerAsync } from '@hooks/queries/governancePower'
import useUserOrDelegator from '@hooks/useUserOrDelegator'
import { fetchProgramVersion } from '@hooks/queries/useProgramVersionQuery'
import { useConnection } from '@solana/wallet-adapter-react'
import { useVotingNfts } from '@hooks/queries/plugins/nftVoter'

interface Props {
className?: string
inAccountDetails?: boolean
children?: React.ReactNode
}

export default function NftVotingPower(props: Props) {
const nfts = useNftPluginStore((s) => s.state.votingNfts)
const { result: votingPower } = useGovernancePowerAsync('community')
const maxWeight = useNftPluginStore((s) => s.state.maxVoteRecord)
const isLoading = useNftPluginStore((s) => s.state.isLoadingNfts)
const Join = () => {
const { connection } = useConnection()
const actingAsWalletPk = useUserOrDelegator()
const wallet = useWalletOnePointOh()
const connected = !!wallet?.connected
const connection = useLegacyConnectionContext()
const ownTokenRecord = useUserCommunityTokenOwnerRecord().data?.result
const realm = useRealmQuery().data?.result
const { realmInfo } = useRealm()

const ownTokenRecord = useUserCommunityTokenOwnerRecord().data?.result
const client = useVotePluginsClientStore(
(s) => s.state.currentRealmVotingClient
)

const displayNfts = nfts.slice(0, 3)
const remainingCount = Math.max(nfts.length - 3, 0)
const max = maxWeight
? new BigNumber(maxWeight.account.maxVoterWeight.toString())
: null
const amount = new BigNumber((votingPower ?? 0).toString())

const handleRegister = async () => {
if (!realm || !wallet?.publicKey || !client.client || !realmInfo)
throw new Error()
if (!realm || !wallet?.publicKey || !client.client) throw new Error()

const programVersion = await fetchProgramVersion(connection, realm.owner)

const instructions: TransactionInstruction[] = []
const { voterWeightPk } = await getVoterWeightRecord(
realm.pubkey,
Expand All @@ -75,7 +69,7 @@ export default function NftVotingPower(props: Props) {
await withCreateTokenOwnerRecord(
instructions,
realm.owner,
realmInfo.programVersion!,
programVersion,
realm.pubkey,
wallet.publicKey,
realm.account.communityMint,
Expand All @@ -87,17 +81,48 @@ export default function NftVotingPower(props: Props) {
await sendTransaction({
transaction: transaction,
wallet: wallet,
connection: connection.current,
connection: connection,
signers: [],
sendingMessage: `Registering`,
successMessage: `Registered`,
})
}

if (isLoading) {
return (
(actingAsWalletPk?.toString === wallet?.publicKey?.toString() &&
connected &&
!ownTokenRecord && (
<Button className="w-full mt-3" onClick={handleRegister}>
Join
</Button>
)) ||
null
)
}

export default function NftVotingPower(props: Props) {
const userPk = useUserOrDelegator()
const nfts = useVotingNfts(userPk)
const {
result: votingPower,
loading: votingPowerLoading,
} = useGovernancePowerAsync('community')
const maxWeight = useNftPluginStore((s) => s.state.maxVoteRecord)

const displayNfts = (nfts ?? []).slice(0, 3)
const remainingCount = Math.max((nfts ?? []).length - 3, 0)
const max = maxWeight
? new BigNumber(maxWeight.account.maxVoterWeight.toString())
: null
const amount = new BigNumber((votingPower ?? 0).toString())

if (votingPowerLoading || nfts === undefined) {
return (
<div
className={classNames(props.className, 'rounded-md bg-bkg-1 h-[76px]')}
className={classNames(
props.className,
'rounded-md bg-bkg-1 h-[76px] animate-pulse'
)}
/>
)
}
Expand Down Expand Up @@ -136,11 +161,7 @@ export default function NftVotingPower(props: Props) {
)}
</div>
</div>
{connected && !ownTokenRecord && (
<Button className="w-full mt-3" onClick={handleRegister}>
Join
</Button>
)}
<Join />
</div>
)
}
53 changes: 4 additions & 49 deletions components/VotePanel/useCanVote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,61 +3,16 @@ 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 { 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'
import { useBatchedVoteDelegators } from './useDelegators'

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 relevantDelegators = useBatchedVoteDelegators(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
const canBatchVote = relevantDelegators && 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 =
Expand Down
Loading

1 comment on commit 085a374

@vercel
Copy link

@vercel vercel bot commented on 085a374 Dec 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

governance-ui – ./

governance-ui-git-main-solana-labs.vercel.app
governance-ui-solana-labs.vercel.app
app.realms.today

Please sign in to comment.