diff --git a/.prettierrc b/.prettierrc index b2095be81e..00fbdb1856 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,4 +1,4 @@ { "semi": false, "singleQuote": true -} +} \ No newline at end of file diff --git a/NftVotePlugin/accounts.ts b/NftVotePlugin/accounts.ts index 76252ed068..048e93d311 100644 --- a/NftVotePlugin/accounts.ts +++ b/NftVotePlugin/accounts.ts @@ -1,5 +1,6 @@ import { NftVoterClient } from '@utils/uiTypes/NftVoterClient' import { PublicKey } from '@solana/web3.js' + export interface NftVoteRecord { account: { governingTokenOwner: PublicKey @@ -45,3 +46,29 @@ export const getNftVoteRecordProgramAddress = async ( nftVoteRecordBump, } } + +export const getNftActionTicketProgramAddress = ( + ticketType: string, + registrar: PublicKey, + owner: PublicKey, + nftMintAddress: string, + clientProgramId: PublicKey +) => { + const [ + nftActionTicket, + nftActionTicketBump, + ] = PublicKey.findProgramAddressSync( + [ + Buffer.from(ticketType), + registrar.toBuffer(), + owner.toBuffer(), + new PublicKey(nftMintAddress).toBuffer(), + ], + clientProgramId + ) + + return { + nftActionTicket, + nftActionTicketBump, + } +} diff --git a/NftVotePlugin/getCnftParamAndProof.ts b/NftVotePlugin/getCnftParamAndProof.ts new file mode 100644 index 0000000000..abdc26da48 --- /dev/null +++ b/NftVotePlugin/getCnftParamAndProof.ts @@ -0,0 +1,88 @@ +import { Connection, PublicKey } from '@solana/web3.js' +import { ConcurrentMerkleTreeAccount } from '@solana/spl-account-compression' +import * as bs58 from 'bs58' +import { fetchDasAssetProofById } from '@hooks/queries/digitalAssets' +import { getNetworkFromEndpoint } from '@utils/connection' +import * as anchor from '@coral-xyz/anchor' + +export function decode(stuff: string) { + return bufferToArray(bs58.decode(stuff)) +} + +function bufferToArray(buffer: Buffer): number[] { + const nums: number[] = [] + for (let i = 0; i < buffer.length; i++) { + nums.push(buffer[i]) + } + return nums +} + +/** + * This is a helper function only for nft-voter-v2 used. + * Given a cNFT, getCnftParamAndProof will to get its the metadata, leaf information and merkle proof. + * All these data will be sent to the program to verify the ownership of the cNFT. + * @param connection + * @param compressedNft + * @returns {param, additionalAccounts} + */ +export async function getCnftParamAndProof( + connection: Connection, + compressedNft: any +) { + const network = getNetworkFromEndpoint(connection.rpcEndpoint) + if (network === 'localnet') throw new Error() + const { result: assetProof } = await fetchDasAssetProofById( + network, + new PublicKey(compressedNft.id) + ) + const treeAccount = await ConcurrentMerkleTreeAccount.fromAccountAddress( + connection, + new PublicKey(compressedNft.compression.tree) + ) + const canopyHeight = treeAccount.getCanopyDepth() + const root = decode(assetProof.root) + const proofLength = assetProof.proof.length + + const reducedProofs = assetProof.proof.slice( + 0, + proofLength - (canopyHeight ? canopyHeight : 0) + ) + + const creators = compressedNft.creators.map((creator) => { + return { + address: new PublicKey(creator.address), + verified: creator.verified, + share: creator.share, + } + }) + + const rawCollection = compressedNft.grouping.find( + (x) => x.group_key === 'collection' + ) + const param = { + name: compressedNft.content.metadata.name, + symbol: compressedNft.content.metadata.symbol, + uri: compressedNft.content.json_uri, + sellerFeeBasisPoints: compressedNft.royalty.basis_points, + primarySaleHappened: compressedNft.royalty.primary_sale_happened, + creators, + isMutable: compressedNft.mutable, + editionNonce: compressedNft.supply.edition_nonce, + collection: { + key: rawCollection ? new PublicKey(rawCollection.group_value) : null, + verified: true, + }, + root, + leafOwner: new PublicKey(compressedNft.ownership.owner), + leafDelegate: new PublicKey( + compressedNft.ownership.delegate || compressedNft.ownership.owner + ), + nonce: new anchor.BN(compressedNft.compression.leaf_id), + index: compressedNft.compression.leaf_id, + proofLen: reducedProofs.length, + } + + const additionalAccounts = [compressedNft.compression.tree, ...reducedProofs] + + return { param: param, additionalAccounts: additionalAccounts } +} diff --git a/NftVotePlugin/store/nftPluginStore.ts b/NftVotePlugin/store/nftPluginStore.ts index 133a171828..4a78f841be 100644 --- a/NftVotePlugin/store/nftPluginStore.ts +++ b/NftVotePlugin/store/nftPluginStore.ts @@ -1,21 +1,23 @@ import { BN } from '@coral-xyz/anchor' import { MaxVoterWeightRecord, ProgramAccount } from '@solana/spl-governance' -import { NFTWithMeta, VotingClient } from '@utils/uiTypes/VotePlugin' +import { VotingClient } from '@utils/uiTypes/VotePlugin' +import { DasNftObject } from '@hooks/queries/digitalAssets' import create, { State } from 'zustand' +import { ON_NFT_VOTER_V2 } from '@constants/flags' interface nftPluginStore extends State { state: { - votingNfts: NFTWithMeta[] + votingNfts: DasNftObject[] votingPower: BN maxVoteRecord: ProgramAccount | null isLoadingNfts: boolean } setVotingNfts: ( - nfts: NFTWithMeta[], + nfts: DasNftObject[], votingClient: VotingClient, nftMintRegistrar: any ) => void - setVotingPower: (nfts: NFTWithMeta[], nftMintRegistrar: any) => void + setVotingPower: (nfts: DasNftObject[], nftMintRegistrar: any) => void setMaxVoterWeight: ( maxVoterRecord: ProgramAccount | null ) => void @@ -47,10 +49,11 @@ const useNftPluginStore = create((set, _get) => ({ }, setVotingPower: (nfts, nftMintRegistrar) => { const votingPower = nfts + .filter((x) => ON_NFT_VOTER_V2 || !x.compression.compressed) .map( (x) => nftMintRegistrar?.collectionConfigs?.find( - (j) => j.collection?.toBase58() === x.collection.mintAddress + (j) => j.collection?.toBase58() === x.grouping[0].group_value )?.weight || new BN(0) ) .reduce((prev, next) => prev.add(next), new BN(0)) diff --git a/actions/castVote.ts b/actions/castVote.ts index abdeac3ef6..04ee37daef 100644 --- a/actions/castVote.ts +++ b/actions/castVote.ts @@ -59,6 +59,9 @@ export async function castVote( const signers: Keypair[] = [] const instructions: TransactionInstruction[] = [] + const createCastNftVoteTicketIxs: TransactionInstruction[] = [] + const createPostMessageTicketIxs: TransactionInstruction[] = [] + const governanceAuthority = walletPubkey const payer = walletPubkey // Explicitly request the version before making RPC calls to work around race conditions in resolving @@ -72,54 +75,54 @@ export async function castVote( const plugin = await votingPlugin?.withCastPluginVote( instructions, proposal, - tokenOwnerRecord + tokenOwnerRecord, + createCastNftVoteTicketIxs ) - const isMulti = proposal.account.voteType !== VoteType.SINGLE_CHOICE; + const isMulti = proposal.account.voteType !== VoteType.SINGLE_CHOICE // It is not clear that defining these extraneous fields, `deny` and `veto`, is actually necessary. // See: https://discord.com/channels/910194960941338677/910630743510777926/1044741454175674378 - const vote = isMulti ? - new Vote({ - voteType: VoteKind.Approve, - approveChoices: proposal.account.options.map((_o, index) => { - if (voteWeights?.includes(index)) { - return new VoteChoice({rank: 0, weightPercentage: 100}); - } else { - return new VoteChoice({rank: 0, weightPercentage: 0}); - } - }), - deny: undefined, - veto: undefined - }) - : - voteKind === VoteKind.Approve - ? new Vote({ - voteType: VoteKind.Approve, - approveChoices: [new VoteChoice({ rank: 0, weightPercentage: 100 })], - deny: undefined, - veto: undefined, - }) - : voteKind === VoteKind.Deny - ? new Vote({ - voteType: VoteKind.Deny, - approveChoices: undefined, - deny: true, - veto: undefined, - }) - : voteKind == VoteKind.Veto - ? new Vote({ - voteType: VoteKind.Veto, - veto: true, - deny: undefined, - approveChoices: undefined, - }) - : new Vote({ - voteType: VoteKind.Abstain, - veto: undefined, - deny: undefined, - approveChoices: undefined, - }) + const vote = isMulti + ? new Vote({ + voteType: VoteKind.Approve, + approveChoices: proposal.account.options.map((_o, index) => { + if (voteWeights?.includes(index)) { + return new VoteChoice({ rank: 0, weightPercentage: 100 }) + } else { + return new VoteChoice({ rank: 0, weightPercentage: 0 }) + } + }), + deny: undefined, + veto: undefined, + }) + : voteKind === VoteKind.Approve + ? new Vote({ + voteType: VoteKind.Approve, + approveChoices: [new VoteChoice({ rank: 0, weightPercentage: 100 })], + deny: undefined, + veto: undefined, + }) + : voteKind === VoteKind.Deny + ? new Vote({ + voteType: VoteKind.Deny, + approveChoices: undefined, + deny: true, + veto: undefined, + }) + : voteKind == VoteKind.Veto + ? new Vote({ + voteType: VoteKind.Veto, + veto: true, + deny: undefined, + approveChoices: undefined, + }) + : new Vote({ + voteType: VoteKind.Abstain, + veto: undefined, + deny: undefined, + approveChoices: undefined, + }) const tokenMint = voteKind === VoteKind.Veto @@ -147,7 +150,8 @@ export async function castVote( const plugin = await votingPlugin?.withUpdateVoterWeightRecord( instructions, tokenOwnerRecord, - 'commentProposal' + 'commentProposal', + createPostMessageTicketIxs ) await withPostChatMessage( @@ -235,17 +239,19 @@ export async function castVote( openNftVotingCountingModal, closeNftVotingCountingModal, } = useNftProposalStore.getState() - //update voter weight + cast vote from spl gov need to be in one transaction - const ixsWithOwnChunk = instructions.slice(-ixChunkCount) - const remainingIxsToChunk = instructions.slice( - 0, - instructions.length - ixChunkCount + + const createNftVoteTicketsChunks = chunks( + [...createCastNftVoteTicketIxs, ...createPostMessageTicketIxs], + 1 ) - const splIxsWithAccountsChunk = chunks(ixsWithOwnChunk, 2) - const nftsAccountsChunks = chunks(remainingIxsToChunk, 2) + const splIxs = instructions.slice(-ixChunkCount) + const nftAccountIxs = instructions.slice(0, -ixChunkCount) + + const splIxsWithAccountsChunk = chunks(splIxs, 2) + const nftsAccountsChunks = chunks(nftAccountIxs, 2) const instructionsChunks = [ - ...nftsAccountsChunks.map((txBatch, batchIdx) => { + ...createNftVoteTicketsChunks.map((txBatch, batchIdx) => { return { instructionsSet: txBatchesToInstructionSetWithSigners( txBatch, @@ -255,6 +261,18 @@ export async function castVote( sequenceType: SequenceType.Parallel, } }), + ...nftsAccountsChunks.map((txBatch, batchIdx) => { + return { + instructionsSet: txBatchesToInstructionSetWithSigners( + txBatch, + [], + batchIdx + ), + sequenceType: + // this is to ensure create all the nft_action_tickets account first + batchIdx == 0 ? SequenceType.Sequential : SequenceType.Parallel, + } + }), ...splIxsWithAccountsChunk.map((txBatch, batchIdx) => { return { instructionsSet: txBatchesToInstructionSetWithSigners( diff --git a/actions/chat/postMessage.ts b/actions/chat/postMessage.ts index 3965d01935..5168450008 100644 --- a/actions/chat/postMessage.ts +++ b/actions/chat/postMessage.ts @@ -1,7 +1,7 @@ import { PublicKey, Keypair, - Transaction, + // Transaction, TransactionInstruction, } from '@solana/web3.js' import { @@ -14,8 +14,13 @@ import { ChatMessageBody } from '@solana/spl-governance' import { withPostChatMessage } from '@solana/spl-governance' import { ProgramAccount } from '@solana/spl-governance' import { RpcContext } from '@solana/spl-governance' -import { sendTransaction } from '../../utils/send' import { VotingClient } from '@utils/uiTypes/VotePlugin' +import { chunks } from '@utils/helpers' +import { + SequenceType, + sendTransactionsV3, + txBatchesToInstructionSetWithSigners, +} from '@utils/sendTransactions' export async function postChatMessage( { connection, wallet, programId, walletPubkey }: RpcContext, @@ -28,6 +33,7 @@ export async function postChatMessage( ) { const signers: Keypair[] = [] const instructions: TransactionInstruction[] = [] + const createNftTicketsIxs: TransactionInstruction[] = [] const governanceAuthority = walletPubkey const payer = walletPubkey @@ -35,7 +41,8 @@ export async function postChatMessage( const plugin = await client?.withUpdateVoterWeightRecord( instructions, tokeOwnerRecord, - 'commentProposal' + 'commentProposal', + createNftTicketsIxs ) await withPostChatMessage( @@ -54,8 +61,43 @@ export async function postChatMessage( plugin?.voterWeightPk ) - const transaction = new Transaction() - transaction.add(...instructions) + // createTicketIxs is a list of instructions that create nftActionTicket only for nft-voter-v2 plugin + // so it will be empty for other plugins or just spl-governance + const nftTicketAccountsChuncks = chunks(createNftTicketsIxs, 1) + + const postMessageIxsChunk = [instructions] + + const instructionsChunks = [ + ...nftTicketAccountsChuncks.map((txBatch, batchIdx) => { + return { + instructionsSet: txBatchesToInstructionSetWithSigners( + txBatch, + [], + batchIdx + ), + sequenceType: SequenceType.Parallel, + } + }), + ...postMessageIxsChunk.map((txBatch, batchIdx) => { + return { + instructionsSet: txBatchesToInstructionSetWithSigners( + txBatch, + [signers], + batchIdx + ), + sequenceType: SequenceType.Sequential, + } + }), + ] + + await sendTransactionsV3({ + connection, + wallet, + transactionInstructions: instructionsChunks, + callbacks: undefined, + }) - await sendTransaction({ transaction, wallet, connection, signers }) + // const transaction = new Transaction() + // transaction.add(...instructions) + // await sendTransaction({ transaction, wallet, connection, signers }) } diff --git a/actions/createProposal.ts b/actions/createProposal.ts index fe03539ba7..e6a6b1dd33 100644 --- a/actions/createProposal.ts +++ b/actions/createProposal.ts @@ -14,7 +14,7 @@ import { InstructionData, withSignOffProposal, withAddSignatory, - MultiChoiceType + MultiChoiceType, } from '@solana/spl-governance' import { sendTransactionsV3, @@ -26,6 +26,7 @@ import { UiInstruction } from '@utils/uiTypes/proposalCreationTypes' import { VotingClient } from '@utils/uiTypes/VotePlugin' import { trySentryLog } from '@utils/logs' import { deduplicateObjsFilter } from '@utils/instructionTools' +import { NftVoterClient } from '@utils/uiTypes/NftVoterClient' export interface InstructionDataWithHoldUpTime { data: InstructionData | null holdUpTime: number | undefined @@ -73,6 +74,7 @@ export const createProposal = async ( callbacks?: Parameters[0]['callbacks'] ): Promise => { const instructions: TransactionInstruction[] = [] + const createNftTicketsIxs: TransactionInstruction[] = [] const governanceAuthority = walletPubkey const signatory = walletPubkey const payer = walletPubkey @@ -91,19 +93,25 @@ export const createProposal = async ( ) // V2 Approve/Deny configuration - const isMulti = options.length > 1; + const isMulti = options.length > 1 const useDenyOption = !isMulti - const voteType = isMulti ? VoteType.MULTI_CHOICE( - MultiChoiceType.FullWeight, 1, options.length, options.length - ) : VoteType.SINGLE_CHOICE - + const voteType = isMulti + ? VoteType.MULTI_CHOICE( + MultiChoiceType.FullWeight, + 1, + options.length, + options.length + ) + : VoteType.SINGLE_CHOICE + //will run only if plugin is connected with realm const plugin = await client?.withUpdateVoterWeightRecord( instructions, tokenOwnerRecord, - 'createProposal' + 'createProposal', + createNftTicketsIxs ) const proposalAddress = await withCreateProposal( @@ -224,27 +232,72 @@ export const createProposal = async ( ...signerChunks, ] - const txes = [ - ...prerequisiteInstructionsChunks, - instructions, - ...insertChunks, - ].map((txBatch, batchIdx) => { - return { - instructionsSet: txBatchesToInstructionSetWithSigners( - txBatch, - signersSet, - batchIdx - ), - sequenceType: SequenceType.Sequential, - } - }) + const isNftVoter = client?.client instanceof NftVoterClient + if (!isNftVoter) { + const txes = [ + ...prerequisiteInstructionsChunks, + instructions, + ...insertChunks, + ].map((txBatch, batchIdx) => { + return { + instructionsSet: txBatchesToInstructionSetWithSigners( + txBatch, + signersSet, + batchIdx + ), + sequenceType: SequenceType.Sequential, + } + }) - await sendTransactionsV3({ - callbacks, - connection, - wallet, - transactionInstructions: txes, - }) + await sendTransactionsV3({ + callbacks, + connection, + wallet, + transactionInstructions: txes, + }) + } + + if (isNftVoter) { + // update voter weight records + const nftTicketAccountsChunk = chunks(createNftTicketsIxs, 1) + const splIxsWithAccountsChunk = [ + ...prerequisiteInstructionsChunks, + instructions, + ...insertChunks, + ] + + const instructionsChunks = [ + ...nftTicketAccountsChunk.map((txBatch, batchIdx) => { + return { + instructionsSet: txBatchesToInstructionSetWithSigners( + txBatch, + [], + batchIdx + ), + sequenceType: SequenceType.Parallel, + } + }), + ...splIxsWithAccountsChunk.map((txBatch, batchIdx) => { + return { + instructionsSet: txBatchesToInstructionSetWithSigners( + txBatch, + signersSet, + batchIdx + ), + sequenceType: SequenceType.Sequential, + } + }), + ] + + // should add checking user has enough sol, refer castVote + + await sendTransactionsV3({ + connection, + wallet, + transactionInstructions: instructionsChunks, + callbacks, + }) + } const logInfo = { realmId: realm.pubkey.toBase58(), diff --git a/components/Members/MemberOverview.tsx b/components/Members/MemberOverview.tsx index f03f4403ea..ed85dbb3dc 100644 --- a/components/Members/MemberOverview.tsx +++ b/components/Members/MemberOverview.tsx @@ -48,7 +48,12 @@ import { import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext' import { useRealmProposalsQuery } from '@hooks/queries/proposal' import useWalletOnePointOh from '@hooks/useWalletOnePointOh' -import { useMembersQuery } from './useMembers' +import { + DasNftObject, + useDigitalAssetsByOwner, +} from '@hooks/queries/digitalAssets' +import { useNftRegistrarCollection } from '@hooks/useNftRegistrarCollection' +import { NFT_PLUGINS_PKS } from '@constants/plugins' const RevokeMembership: FC<{ member: PublicKey; mint: PublicKey }> = ({ member, @@ -109,7 +114,117 @@ const RevokeMembership: FC<{ member: PublicKey; mint: PublicKey }> = ({ ) } -const MemberOverview = ({ member }: { member: Member }) => { +const getNftMetadataTooltip = (nft) => { + // console.log(nft) + const collection = nft.grouping.find((x) => x.group_key === 'collection') + ?.group_value + return ( +
+
+ {nft.compression.compressed ? 'Compressed NFT' : 'NFT'} +
+
+

+ Name: {nft.content.metadata.name} +

+

+ Symbol:{' '} + {nft.content.metadata.symbol} +

+

+ Description:{' '} + {nft.content.metadata.description} +

+

+ {nft.compression.compressed ? 'Asset ID: ' : 'Address: '}{' '} + {nft.id} +

+

+ Ownership: {nft.ownership.owner} +

+

+ Collection: {collection} +

+
+
+ ) +} + +const NftDisplayList = ({ + member, + communityAmount, + councilAmount, +}: { + member: Member + communityAmount: string + councilAmount: string +}) => { + const { data: nfts, isLoading } = useDigitalAssetsByOwner( + new PublicKey(member.walletAddress) + ) + + const usedCollectionsPks: string[] = useNftRegistrarCollection() + const verifiedNfts: DasNftObject[] | undefined = useMemo( + () => + nfts?.filter((nft) => { + const collection = nft.grouping.find( + (x) => x.group_key === 'collection' + ) + return ( + collection && + usedCollectionsPks.includes(collection.group_value) && + nft.creators?.filter((x) => x.verified).length > 0 + ) + }), + [nfts, usedCollectionsPks] + ) + + if (isLoading) { + return ( +
+
+ Loading NFTs... +
+
+ ) + } + + if (!verifiedNfts || verifiedNfts.length === 0) { + return ( +
+
+ Something went wrong, fail to fetch.. +
+
+ ) + } + + return ( +
+
+ {(communityAmount || !councilAmount) && ( + + {verifiedNfts?.map((nft) => { + return ( + + + + ) + })} + + )} +
+
+ ) +} + +const MemberOverview = ({ + member, + activeMembers, +}: { + member: Member + activeMembers: any[] | undefined +}) => { const programVersion = useProgramVersion() const realm = useRealmQuery().data?.result const config = useRealmConfigQuery().data?.result @@ -127,8 +242,14 @@ const MemberOverview = ({ member }: { member: Member }) => { ), [proposalsArray] ) + + const currentPluginPk = config?.account.communityTokenConfig.voterWeightAddin + const isNftMode = + (currentPluginPk && + NFT_PLUGINS_PKS.includes(currentPluginPk?.toBase58())) || + false + const { fmtUrlWithCluster } = useQueryContext() - const { data: activeMembers } = useMembersQuery() const [ownVoteRecords, setOwnVoteRecords] = useState< WalletTokenRecordWithProposal[] >([]) @@ -358,6 +479,14 @@ const MemberOverview = ({ member }: { member: Member }) => { + {isNftMode && ( + + )} +

{ownVoteRecords?.length} Recent Votes diff --git a/components/Members/MembersTabs.tsx b/components/Members/MembersTabs.tsx index 4a2cf60761..eb1704533f 100644 --- a/components/Members/MembersTabs.tsx +++ b/components/Members/MembersTabs.tsx @@ -12,6 +12,8 @@ import { useRealmCouncilMintInfoQuery, } from '@hooks/queries/mintInfo' import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext' +import { useRealmConfigQuery } from '@hooks/queries/realmConfig' +import { NFT_PLUGINS_PKS } from '@constants/plugins' interface MembersTabsProps { activeTab: Member @@ -27,10 +29,20 @@ const MembersTabs: FunctionComponent = ({ const realm = useRealmQuery().data?.result const mint = useRealmCommunityMintInfoQuery().data?.result const councilMint = useRealmCouncilMintInfoQuery().data?.result + + const config = useRealmConfigQuery().data?.result + const currentPluginPk = config?.account.communityTokenConfig.voterWeightAddin + const isNftMode = + (currentPluginPk && + NFT_PLUGINS_PKS.includes(currentPluginPk?.toBase58())) || + false + const tokenName = realm ? tokenPriceService.getTokenInfo(realm?.account.communityMint.toBase58()) ?.symbol : '' + + const nftName = isNftMode ? 'NFT' : undefined return (
= ({ mint={mint} councilMint={councilMint} activeTab={activeTab} - tokenName={tokenName || ''} + tokenName={tokenName || nftName || ''} onChange={onChange} > ) diff --git a/components/Members/useMembers.tsx b/components/Members/useMembers.tsx index 8b25b57a2a..f4579d56bf 100644 --- a/components/Members/useMembers.tsx +++ b/components/Members/useMembers.tsx @@ -16,8 +16,14 @@ import { capitalize } from '@utils/helpers' import { Member } from 'utils/uiTypes/members' import { useRealmQuery } from '@hooks/queries/realm' import { useTokenOwnerRecordsForRealmQuery } from '@hooks/queries/tokenOwnerRecord' -import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext' import { useQuery } from '@tanstack/react-query' +import { useConnection } from '@solana/wallet-adapter-react' +import { useRealmConfigQuery } from '@hooks/queries/realmConfig' +import { NFT_PLUGINS_PKS } from '@constants/plugins' +import { useNftRegistrarCollection } from '@hooks/useNftRegistrarCollection' +import { fetchDigitalAssetsByOwner } from '@hooks/queries/digitalAssets' +import { getNetworkFromEndpoint } from '@utils/connection' +import { BN } from '@coral-xyz/anchor' /** * @deprecated @@ -27,9 +33,19 @@ export const useMembersQuery = () => { const realm = useRealmQuery().data?.result const { data: tors } = useTokenOwnerRecordsForRealmQuery() - const connection = useLegacyConnectionContext() + const connection = useConnection() + const config = useRealmConfigQuery().data?.result + const currentPluginPk = config?.account.communityTokenConfig.voterWeightAddin + const isNftMode = + currentPluginPk && NFT_PLUGINS_PKS.includes(currentPluginPk?.toBase58()) + const usedCollectionsPks = useNftRegistrarCollection() - const enabled = tors !== undefined && realm !== undefined + const network = getNetworkFromEndpoint(connection.connection.rpcEndpoint) + if (network === 'localnet') throw new Error() + const enabled = + tors !== undefined && + realm !== undefined && + (!isNftMode || usedCollectionsPks !== undefined) const query = useQuery({ enabled, @@ -41,13 +57,41 @@ export const useMembersQuery = () => { const communityMint = realm.account.communityMint - const tokenRecordArray = tors - .filter((x) => x.account.governingTokenMint.equals(communityMint)) - .map((x) => ({ - walletAddress: x.account.governingTokenOwner.toString(), - community: x, - _kind: 'community' as const, - })) + const tokenRecordArray = isNftMode + ? await Promise.all( + tors.map(async (x) => { + const ownedNfts = await fetchDigitalAssetsByOwner( + network, + x.account.governingTokenOwner + ) + + const verifiedNfts = ownedNfts.filter((nft) => { + const collection = nft.grouping.find( + (x) => x.group_key === 'collection' + ) + return ( + collection && + usedCollectionsPks.includes(collection.group_value) + ) + }) + + x.account.governingTokenDepositAmount = new BN( // maybe should use add? + verifiedNfts.length * 10 ** 6 + ) + return { + walletAddress: x.account.governingTokenOwner.toString(), + community: x, + _kind: 'community' as const, + } + }) + ) + : tors + .filter((x) => x.account.governingTokenMint.equals(communityMint)) + .map((x) => ({ + walletAddress: x.account.governingTokenOwner.toString(), + community: x, + _kind: 'community' as const, + })) const councilRecordArray = councilMint !== undefined @@ -63,7 +107,7 @@ export const useMembersQuery = () => { const fetchCouncilMembersWithTokensOutsideRealm = async () => { if (realm?.account.config.councilMint) { const tokenAccounts = await getTokenAccountsByMint( - connection.current, + connection.connection, realm.account.config.councilMint.toBase58() ) const tokenAccountsInfo: TokenProgramAccount[] = [] @@ -102,7 +146,7 @@ export const useMembersQuery = () => { ATAS.push(ata) } const ownersAtas = await getMultipleAccountInfoChunked( - connection.current, + connection.connection, ATAS ) const ownersAtasParsed: TokenProgramAccount[] = ownersAtas @@ -212,7 +256,6 @@ export const useMembersQuery = () => { console.log('useMembers is fetching') let members = [...membersWithTokensDeposited] - const [councilMembers, communityMembers] = await Promise.all([ fetchCouncilMembersWithTokensOutsideRealm(), fetchCommunityMembersATAS(), @@ -224,7 +267,6 @@ export const useMembersQuery = () => { const activeMembers = members.filter( (x) => !x.councilVotes.isZero() || !x.communityVotes.isZero() ) - return activeMembers }, }) diff --git a/components/NewRealmWizard/components/NFTCollectionModal.tsx b/components/NewRealmWizard/components/NFTCollectionModal.tsx index 673bdee156..e1c5d8ab97 100644 --- a/components/NewRealmWizard/components/NFTCollectionModal.tsx +++ b/components/NewRealmWizard/components/NFTCollectionModal.tsx @@ -8,6 +8,7 @@ import { WalletIcon } from './steps/AddNFTCollectionForm' import Modal from '@components/Modal' import { + DasNftObject, dasByIdQueryFn, digitalAssetsQueryKeys, useDigitalAssetsByOwner, @@ -17,8 +18,9 @@ import { useAsync } from 'react-async-hook' import queryClient from '@hooks/queries/queryClient' import { getNetworkFromEndpoint } from '@utils/connection' import { PublicKey } from '@solana/web3.js' +import { ON_NFT_VOTER_V2 } from '@constants/flags' -function filterAndMapVerifiedCollections(nfts) { +function filterAndMapVerifiedCollections(nfts: DasNftObject[]) { return nfts ?.filter((nft) => { if ( @@ -30,6 +32,7 @@ function filterAndMapVerifiedCollections(nfts) { return false } }) + .filter((nft) => ON_NFT_VOTER_V2 || !nft.compression.compressed) .reduce((prev, curr) => { const collectionKey = curr.grouping.find( (x) => x.group_key === 'collection' diff --git a/components/ProposalVoterNftChart.tsx b/components/ProposalVoterNftChart.tsx new file mode 100644 index 0000000000..14d60cc72a --- /dev/null +++ b/components/ProposalVoterNftChart.tsx @@ -0,0 +1,81 @@ +import { NFT_PLUGINS_PKS } from '@constants/plugins' +import { useDigitalAssetsByOwner } from '@hooks/queries/digitalAssets' +import { useRealmConfigQuery } from '@hooks/queries/realmConfig' +import { useNftRegistrarCollection } from '@hooks/useNftRegistrarCollection' +import { PublicKey } from '@solana/web3.js' +import { useMemo } from 'react' + +interface Props { + className?: string + voteType?: number + highlighted?: string +} + +function filterVerifiedCollections(nfts, usedCollectionsPks) { + return nfts?.filter((nft) => { + const collection = nft.grouping.find((x) => x.group_key === 'collection') + return ( + collection && + usedCollectionsPks.includes(collection.group_value) && + nft.creators?.filter((x) => x.verified).length > 0 + ) + }) +} + +const ProposalVoterNftChart = (props: Props) => { + const config = useRealmConfigQuery().data?.result + const currentPluginPk = config?.account.communityTokenConfig.voterWeightAddin + const isNftMode = + currentPluginPk && NFT_PLUGINS_PKS.includes(currentPluginPk?.toBase58()) + + const { data: nfts, isLoading } = useDigitalAssetsByOwner( + props.highlighted ? new PublicKey(props.highlighted) : undefined + ) + + const usedCollectionsPks: string[] = useNftRegistrarCollection() + const verifiedNfts = useMemo( + () => filterVerifiedCollections(nfts, usedCollectionsPks), + [nfts, usedCollectionsPks] + ) + + if (!isNftMode) return <> + + return ( +
+

+ Voter's NFTs +

+ {!props.highlighted ? ( +
Move cursor to an account
+ ) : isLoading ? ( +
Loading NFTs...
+ ) : !verifiedNfts || verifiedNfts.length === 0 ? ( +
+ Something went wrong, fail to fetch... +
+ ) : ( +
+ {verifiedNfts && + verifiedNfts.map((nft: any) => { + return ( + {nft.content.metadata.name} + ) + })} +
+ )} +
+ ) +} + +export default ProposalVoterNftChart diff --git a/components/ProposalVotingPower/NftVotingPower.tsx b/components/ProposalVotingPower/NftVotingPower.tsx index 7a3d80404d..810b3ca353 100644 --- a/components/ProposalVotingPower/NftVotingPower.tsx +++ b/components/ProposalVotingPower/NftVotingPower.tsx @@ -123,8 +123,10 @@ export default function NftVotingPower(props: Props) { {displayNfts.slice(0, 3).map((nft, index) => (
))} {!!remainingCount && ( diff --git a/components/RealmHeader.tsx b/components/RealmHeader.tsx index 0d12f0955b..f319c8d8cb 100644 --- a/components/RealmHeader.tsx +++ b/components/RealmHeader.tsx @@ -10,6 +10,7 @@ import { getRealmExplorerHost } from 'tools/routing' import { tryParsePublicKey } from '@tools/core/pubkey' import { useRealmQuery } from '@hooks/queries/realm' import { useRealmConfigQuery } from '@hooks/queries/realmConfig' +import { NFT_PLUGINS_PKS } from '@constants/plugins' const RealmHeader = () => { const { fmtUrlWithCluster } = useQueryContext() @@ -67,7 +68,10 @@ const RealmHeader = () => {
)}
- {!config?.account.communityTokenConfig.voterWeightAddin && ( + {(!config?.account.communityTokenConfig.voterWeightAddin || + NFT_PLUGINS_PKS.includes( + config?.account.communityTokenConfig.voterWeightAddin.toBase58() + )) && ( diff --git a/constants/flags.ts b/constants/flags.ts index 436f5fec5f..0f9b10bdaf 100644 --- a/constants/flags.ts +++ b/constants/flags.ts @@ -1 +1,2 @@ export const SUPPORT_CNFTS = true +export const ON_NFT_VOTER_V2 = false diff --git a/constants/plugins.ts b/constants/plugins.ts index b3e5733bd9..7f7808a8a4 100644 --- a/constants/plugins.ts +++ b/constants/plugins.ts @@ -16,9 +16,10 @@ export const HELIUM_VSR_PLUGINS_PKS: string[] = [ export const NFT_PLUGINS_PKS: string[] = [ DEFAULT_NFT_VOTER_PLUGIN, 'GnftV5kLjd67tvHpNGyodwWveEKivz3ZWvvE3Z4xi2iw', + 'GnftVc21v2BRchsRa9dGdrVmJPLZiRHe9j2offnFTZFg', // v2, supporting compressed nft ] export const GATEWAY_PLUGINS_PKS: string[] = [ 'Ggatr3wgDLySEwA2qEjt1oiw4BUzp5yMLJyz21919dq6', 'GgathUhdrCWRHowoRKACjgWhYHfxCEdBi5ViqYN6HVxk', // v2, supporting composition -] \ No newline at end of file +] diff --git a/hooks/queries/digitalAssets.ts b/hooks/queries/digitalAssets.ts index 8159a36115..932876d77d 100644 --- a/hooks/queries/digitalAssets.ts +++ b/hooks/queries/digitalAssets.ts @@ -80,6 +80,11 @@ export type DasNftObject = { image: string } } + creators: { + address: string + share: number + verified: boolean + }[] id: string } diff --git a/hooks/useNftRegistrarCollection.ts b/hooks/useNftRegistrarCollection.ts new file mode 100644 index 0000000000..81d2e65638 --- /dev/null +++ b/hooks/useNftRegistrarCollection.ts @@ -0,0 +1,23 @@ +import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' +import { useRealmConfigQuery } from './queries/realmConfig' +import { useMemo } from 'react' +import { NFT_PLUGINS_PKS } from '@constants/plugins' + +export const useNftRegistrarCollection = () => { + const config = useRealmConfigQuery().data?.result + const currentPluginPk = config?.account.communityTokenConfig.voterWeightAddin + const [nftMintRegistrar] = useVotePluginsClientStore((s) => [ + s.state.nftMintRegistrar, + ]) + + return useMemo( + () => + (currentPluginPk && + NFT_PLUGINS_PKS.includes(currentPluginPk?.toBase58()) && + nftMintRegistrar?.collectionConfigs.map((x) => + x.collection.toBase58() + )) ?? + [], + [currentPluginPk, nftMintRegistrar?.collectionConfigs] + ) +} diff --git a/hooks/useVoteRecords.ts b/hooks/useVoteRecords.ts index e1a9b06ba2..647695d817 100644 --- a/hooks/useVoteRecords.ts +++ b/hooks/useVoteRecords.ts @@ -22,6 +22,11 @@ import { PublicKey } from '@solana/web3.js' import { useRealmQuery } from './queries/realm' import { useRealmCommunityMintInfoQuery } from './queries/mintInfo' import useLegacyConnectionContext from './useLegacyConnectionContext' +import { calculateMaxVoteScore } from '@models/proposal/calulateMaxVoteScore' +import { getNetworkFromEndpoint } from '@utils/connection' +import { fetchDigitalAssetsByOwner } from './queries/digitalAssets' +import { useNftRegistrarCollection } from './useNftRegistrarCollection' +import { useAsync } from 'react-async-hook' export default function useVoteRecords(proposal?: ProgramAccount) { const { getRpcContext } = useRpcContext() @@ -33,7 +38,7 @@ export default function useVoteRecords(proposal?: ProgramAccount) { >([]) const realm = useRealmQuery().data?.result const mint = useRealmCommunityMintInfoQuery().data?.result - const { vsrMode } = useRealm() + const { vsrMode, isNftMode } = useRealm() //for vsr const [ @@ -51,6 +56,61 @@ export default function useVoteRecords(proposal?: ProgramAccount) { const connection = useLegacyConnectionContext() const governingTokenMintPk = proposal?.account.governingTokenMint + // for nft-voter + // This part is to get the undecided nft-voter information for each proposal. + // In buildTopVoters.ts, it checks whether the token_owner_record is in the vote_record. + // If not, the function use record.account.governingTokenDepositAmount as the undecided vote weight, where nft-voter should be 0. + // Thus, pre-calculating the undecided weight for each nft voter is necessary. + const [nftMintRegistrar] = useVotePluginsClientStore((s) => [ + s.state.nftMintRegistrar, + ]) + const usedCollectionsPks: string[] = useNftRegistrarCollection() + + const { result: undecidedNftsByVoteRecord } = useAsync(async () => { + const network = getNetworkFromEndpoint(connection.endpoint) + const enabled = + isNftMode && usedCollectionsPks !== undefined && network !== 'localnet' + if (!enabled) return {} + + // this filter out the token_owner_record that has already voted + const undecidedVoter = tokenOwnerRecords.filter( + (tokenOwnerRecord) => + !voteRecords + .filter((x) => x.account.vote?.voteType !== VoteKind.Veto) + .some( + (voteRecord) => + voteRecord.account.governingTokenOwner.toBase58() === + tokenOwnerRecord.account.governingTokenOwner.toBase58() + ) + ) + + // get every nft owned by the undecided voter, then sum it up as the undecided weight(voting power) + const walletsPks = undecidedVoter.map((x) => x.account.governingTokenOwner) + const undecidedVoters = await Promise.all( + walletsPks.map(async (walletPk) => { + const ownedNfts = await fetchDigitalAssetsByOwner(network, walletPk) + const verifiedNfts = ownedNfts.filter((nft) => { + const collection = nft.grouping.find( + (x) => x.group_key === 'collection' + ) + return ( + collection && usedCollectionsPks.includes(collection.group_value) + ) + }) + return { + walletPk: walletPk, + votingPower: verifiedNfts.length * 10 ** 6, //default decimal wieight is 10^6 + } + }) + ) + // make it a dictionary structure + const undecidedNftsByVoteRecord = Object.fromEntries( + undecidedVoters.map((x) => [x.walletPk.toBase58(), new BN(x.votingPower)]) + ) + return undecidedNftsByVoteRecord + }, [tokenOwnerRecords, voteRecords, usedCollectionsPks, isNftMode]) + /// + useEffect(() => { if (context && proposal && realm) { // fetch vote records @@ -92,17 +152,34 @@ export default function useVoteRecords(proposal?: ProgramAccount) { } }, [getRpcContext]) const topVoters = useMemo(() => { - if (realm && proposal && mint) { + if (realm && proposal && mint && !isNftMode) { + const maxVote = calculateMaxVoteScore(realm, proposal, mint) return buildTopVoters( voteRecords, tokenOwnerRecords, - realm, - proposal, mint, - undecidedDepositByVoteRecord + undecidedDepositByVoteRecord, + maxVote + ) + } else if (realm && proposal && mint && isNftMode) { + const nftVoterPluginTotalWeight = nftMintRegistrar?.collectionConfigs.reduce( + (prev, curr) => { + const size = curr.size + const weight = curr.weight + if (typeof size === 'undefined' || typeof weight === 'undefined') + return prev + return prev + size * weight + }, + 0 + ) + return buildTopVoters( + voteRecords, + tokenOwnerRecords, + mint, + undecidedNftsByVoteRecord ?? {}, + new BN(nftVoterPluginTotalWeight) ) } - return [] }, [ voteRecords, @@ -111,6 +188,9 @@ export default function useVoteRecords(proposal?: ProgramAccount) { proposal, mint, undecidedDepositByVoteRecord, + undecidedNftsByVoteRecord, + isNftMode, + nftMintRegistrar, ]) useEffect(() => { diff --git a/hooks/useVotingPlugins.ts b/hooks/useVotingPlugins.ts index 4fbb9022c3..9cafeae390 100644 --- a/hooks/useVotingPlugins.ts +++ b/hooks/useVotingPlugins.ts @@ -1,5 +1,4 @@ import { useCallback, useEffect, useMemo } from 'react' -import { getNfts } from '@utils/tokens' import useNftPluginStore from 'NftVotePlugin/store/nftPluginStore' import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' import { getMaxVoterWeightRecord } from '@solana/spl-governance' @@ -8,7 +7,7 @@ import { notify } from '@utils/notifications' import useGatewayPluginStore from '../GatewayPlugin/store/gatewayPluginStore' import { getGatekeeperNetwork } from '../GatewayPlugin/sdk/accounts' -import { NFTWithMeta } from '@utils/uiTypes/VotePlugin' +import { DasNftObject } from '@hooks/queries/digitalAssets' import useHeliumVsrStore from 'HeliumVotePlugin/hooks/useHeliumVsrStore' import * as heliumVsrSdk from '@helium/voter-stake-registry-sdk' import useWalletOnePointOh from './useWalletOnePointOh' @@ -22,6 +21,9 @@ import { GATEWAY_PLUGINS_PKS, } from '../constants/plugins' import useUserOrDelegator from './useUserOrDelegator' +import { getNetworkFromEndpoint } from '@utils/connection' +import { fetchDigitalAssetsByOwner } from './queries/digitalAssets' +import { ON_NFT_VOTER_V2, SUPPORT_CNFTS } from '@constants/flags' export function useVotingPlugins() { const realm = useRealmQuery().data?.result @@ -114,14 +116,13 @@ export function useVotingPlugins() { ]) const getIsFromCollection = useCallback( - (nft: NFTWithMeta) => { + (nft: DasNftObject) => { + const collection = nft.grouping.find((x) => x.group_key === 'collection') return ( - nft.collection && - nft.collection.mintAddress && - (nft.collection.verified || - typeof nft.collection.verified === 'undefined') && - usedCollectionsPks.includes(nft.collection.mintAddress) && - nft.collection.creators?.filter((x) => x.verified).length > 0 + (SUPPORT_CNFTS || !nft.compression.compressed) && + collection && + usedCollectionsPks.includes(collection.group_value) && + nft.creators?.filter((x) => x.verified).length > 0 ) }, [usedCollectionsPks] @@ -327,8 +328,13 @@ export function useVotingPlugins() { setIsLoadingNfts(true) if (!wallet?.publicKey) return try { - const nfts = await getNfts(wallet.publicKey, connection) - const votingNfts = nfts.filter(getIsFromCollection) + // const nfts = await getNfts(wallet.publicKey, connection) + const network = getNetworkFromEndpoint(connection.endpoint) + if (network === 'localnet') throw new Error() + const nfts = await fetchDigitalAssetsByOwner(network, wallet.publicKey) + const votingNfts = nfts + .filter(getIsFromCollection) + .filter((x) => ON_NFT_VOTER_V2 || !x.compression.compressed) const nftsWithMeta = votingNfts setVotingNfts(nftsWithMeta, currentClient, nftMintRegistrar) } catch (e) { diff --git a/hub/components/NewWallet/useNewWalletTransaction.ts b/hub/components/NewWallet/useNewWalletTransaction.ts index 0bc09edd21..14db1f817b 100644 --- a/hub/components/NewWallet/useNewWalletTransaction.ts +++ b/hub/components/NewWallet/useNewWalletTransaction.ts @@ -13,8 +13,13 @@ import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext'; import useProgramVersion from '@hooks/useProgramVersion'; import useRealm from '@hooks/useRealm'; import useWalletOnePointOh from '@hooks/useWalletOnePointOh'; +import { chunks } from '@utils/helpers'; import { trySentryLog } from '@utils/logs'; -import { SequenceType, sendTransactionsV3 } from '@utils/sendTransactions'; +import { + SequenceType, + sendTransactionsV3, + txBatchesToInstructionSetWithSigners, +} from '@utils/sendTransactions'; import useGovernanceDefaults from './useGovernanceDefaults'; @@ -51,12 +56,14 @@ const useNewWalletCallback = ( ); const instructions: TransactionInstruction[] = []; + const createNftTicketsIxs: TransactionInstruction[] = []; // client is typed such that it cant be undefined, but whatever. const plugin = await client?.withUpdateVoterWeightRecord( instructions, tokenOwnerRecord, 'createGovernance', + createNftTicketsIxs, ); const governanceAddress = await withCreateGovernance( @@ -79,8 +86,22 @@ const useNewWalletCallback = ( wallet.publicKey, ); + // createTicketIxs is a list of instructions that create nftActionTicket only for nft-voter-v2 plugin + // so it will be empty for other plugins + const nftTicketAccountsChuncks = chunks(createNftTicketsIxs, 1); + await sendTransactionsV3({ transactionInstructions: [ + ...nftTicketAccountsChuncks.map((txBatch, batchIdx) => { + return { + instructionsSet: txBatchesToInstructionSetWithSigners( + txBatch, + [], + batchIdx, + ), + sequenceType: SequenceType.Parallel, + }; + }), { instructionsSet: instructions.map((x) => ({ transactionInstruction: x, diff --git a/hub/providers/Proposal/createProposal.ts b/hub/providers/Proposal/createProposal.ts index 7520af53e9..3a2e809394 100644 --- a/hub/providers/Proposal/createProposal.ts +++ b/hub/providers/Proposal/createProposal.ts @@ -1,3 +1,4 @@ +import { SUPPORT_CNFTS } from '@constants/flags'; import { VSR_PLUGIN_PKS, NFT_PLUGINS_PKS, @@ -30,9 +31,13 @@ import { } from 'actions/createProposal'; import { tryGetNftRegistrar } from 'VoteStakeRegistry/sdk/api'; +import { + DasNftObject, + fetchDigitalAssetsByOwner, +} from '@hooks/queries/digitalAssets'; +import { getNetworkFromEndpoint } from '@utils/connection'; import { getRegistrarPDA as getPluginRegistrarPDA } from '@utils/plugin/accounts'; -import { getNfts } from '@utils/tokens'; -import { NFTWithMeta, VotingClient } from '@utils/uiTypes/VotePlugin'; +import { VotingClient } from '@utils/uiTypes/VotePlugin'; import { fetchPlugins } from './fetchPlugins'; @@ -172,7 +177,7 @@ export async function createProposal(args: Args) { const pluginPublicKey = realmConfig.account.communityTokenConfig.voterWeightAddin; let votingClient: VotingClient | undefined = undefined; - let votingNfts: NFTWithMeta[] = []; + let votingNfts: DasNftObject[] = []; if (pluginPublicKey) { const votingPlugins = await fetchPlugins( @@ -218,19 +223,27 @@ export async function createProposal(args: Args) { x.collection.toBase58(), ) || []; - const nfts = await getNfts(args.requestingUserPublicKey, { - cluster: args.cluster, - } as any); - - votingNfts = nfts.filter( - (nft) => - nft.collection && - nft.collection.mintAddress && - (nft.collection.verified || - typeof nft.collection.verified === 'undefined') && - collections.includes(nft.collection.mintAddress) && - nft.collection.creators?.filter((x) => x.verified).length > 0, + // const nfts = await getNfts(args.requestingUserPublicKey, { + // cluster: args.cluster, + // } as any); + const network = getNetworkFromEndpoint(args.connection.rpcEndpoint); + if (network === 'localnet') throw new Error(); + const nfts = await fetchDigitalAssetsByOwner( + network, + args.requestingUserPublicKey, ); + + votingNfts = nfts.filter((nft) => { + const collection = nft.grouping.find( + (x: any) => x.group_key === 'collection', + ); + return ( + (SUPPORT_CNFTS || !nft.compression.compressed) && + collection && + collections.includes(collection.group_value) && + nft.creators?.filter((x: any) => x.verified).length > 0 + ); + }); } } diff --git a/idls/nft_voter_v2.ts b/idls/nft_voter_v2.ts new file mode 100644 index 0000000000..11f077de7d --- /dev/null +++ b/idls/nft_voter_v2.ts @@ -0,0 +1,2163 @@ +export type NftVoterV2 = { + "version": "0.2.3", + "name": "nft_voter", + "instructions": [ + { + "name": "createRegistrar", + "accounts": [ + { + "name": "registrar", + "isMut": true, + "isSigner": false, + "docs": [ + "The NFT voting Registrar", + "There can only be a single registrar per governance Realm and governing mint of the Realm" + ] + }, + { + "name": "governanceProgramId", + "isMut": false, + "isSigner": false, + "docs": [ + "The program id of the spl-governance program the realm belongs to" + ] + }, + { + "name": "realm", + "isMut": false, + "isSigner": false, + "docs": [ + "An spl-governance Realm", + "", + "Realm is validated in the instruction:", + "- Realm is owned by the governance_program_id", + "- governing_token_mint must be the community or council mint", + "- realm_authority is realm.authority" + ] + }, + { + "name": "governingTokenMint", + "isMut": false, + "isSigner": false, + "docs": [ + "Either the realm community mint or the council mint.", + "It must match Realm.community_mint or Realm.config.council_mint", + "", + "Note: Once the NFT plugin is enabled the governing_token_mint is used only as identity", + "for the voting population and the tokens of that are no longer used" + ] + }, + { + "name": "realmAuthority", + "isMut": false, + "isSigner": true, + "docs": [ + "realm_authority must sign and match Realm.authority" + ] + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "maxCollections", + "type": "u8" + } + ] + }, + { + "name": "createVoterWeightRecord", + "accounts": [ + { + "name": "voterWeightRecord", + "isMut": true, + "isSigner": false + }, + { + "name": "governanceProgramId", + "isMut": false, + "isSigner": false, + "docs": [ + "The program id of the spl-governance program the realm belongs to" + ] + }, + { + "name": "realm", + "isMut": false, + "isSigner": false + }, + { + "name": "realmGoverningTokenMint", + "isMut": false, + "isSigner": false, + "docs": [ + "Either the realm community mint or the council mint." + ] + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "governingTokenOwner", + "type": "publicKey" + } + ] + }, + { + "name": "createMaxVoterWeightRecord", + "accounts": [ + { + "name": "maxVoterWeightRecord", + "isMut": true, + "isSigner": false + }, + { + "name": "governanceProgramId", + "isMut": false, + "isSigner": false, + "docs": [ + "The program id of the spl-governance program the realm belongs to" + ] + }, + { + "name": "realm", + "isMut": false, + "isSigner": false + }, + { + "name": "realmGoverningTokenMint", + "isMut": false, + "isSigner": false, + "docs": [ + "Either the realm community mint or the council mint." + ] + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "updateVoterWeightRecord", + "accounts": [ + { + "name": "registrar", + "isMut": false, + "isSigner": false, + "docs": [ + "The NFT voting Registrar" + ] + }, + { + "name": "voterWeightRecord", + "isMut": true, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + } + ], + "args": [ + { + "name": "voterWeightAction", + "type": { + "defined": "VoterWeightAction" + } + } + ] + }, + { + "name": "relinquishNftVote", + "accounts": [ + { + "name": "registrar", + "isMut": false, + "isSigner": false, + "docs": [ + "The NFT voting Registrar" + ] + }, + { + "name": "voterWeightRecord", + "isMut": true, + "isSigner": false + }, + { + "name": "governance", + "isMut": false, + "isSigner": false, + "docs": [ + "Governance account the Proposal is for" + ] + }, + { + "name": "proposal", + "isMut": false, + "isSigner": false + }, + { + "name": "voterTokenOwnerRecord", + "isMut": false, + "isSigner": false, + "docs": [ + "TokenOwnerRecord of the voter who cast the original vote" + ] + }, + { + "name": "voterAuthority", + "isMut": false, + "isSigner": true, + "docs": [ + "Authority of the voter who cast the original vote", + "It can be either governing_token_owner or its delegate and must sign this instruction" + ] + }, + { + "name": "voteRecord", + "isMut": false, + "isSigner": false, + "docs": [ + "The account is used to validate that it doesn't exist and if it doesn't then Anchor owner check throws error", + "The check is disabled here and performed inside the instruction", + "#[account(owner = registrar.governance_program_id)]" + ] + }, + { + "name": "beneficiary", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "configureCollection", + "accounts": [ + { + "name": "registrar", + "isMut": true, + "isSigner": false, + "docs": [ + "Registrar for which we configure this Collection" + ] + }, + { + "name": "realm", + "isMut": false, + "isSigner": false + }, + { + "name": "realmAuthority", + "isMut": false, + "isSigner": true, + "docs": [ + "Authority of the Realm must sign and match Realm.authority" + ] + }, + { + "name": "collection", + "isMut": false, + "isSigner": false + }, + { + "name": "maxVoterWeightRecord", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "weight", + "type": "u64" + }, + { + "name": "size", + "type": "u32" + } + ] + }, + { + "name": "castNftVote", + "accounts": [ + { + "name": "registrar", + "isMut": false, + "isSigner": false, + "docs": [ + "The NFT voting registrar" + ] + }, + { + "name": "voterWeightRecord", + "isMut": true, + "isSigner": false + }, + { + "name": "voterTokenOwnerRecord", + "isMut": false, + "isSigner": false, + "docs": [ + "TokenOwnerRecord of the voter who casts the vote", + "/// CHECK: Owned by spl-governance instance specified in registrar.governance_program_id" + ] + }, + { + "name": "voterAuthority", + "isMut": false, + "isSigner": true, + "docs": [ + "Authority of the voter who casts the vote", + "It can be either governing_token_owner or its delegate and must sign this instruction" + ] + }, + { + "name": "payer", + "isMut": true, + "isSigner": true, + "docs": [ + "The account which pays for the transaction" + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "proposal", + "type": "publicKey" + } + ] + }, + { + "name": "createNftActionTicket", + "accounts": [ + { + "name": "registrar", + "isMut": false, + "isSigner": false + }, + { + "name": "voterWeightRecord", + "isMut": true, + "isSigner": false + }, + { + "name": "voterAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "voterWeightAction", + "type": { + "defined": "VoterWeightAction" + } + } + ] + }, + { + "name": "createCnftActionTicket", + "accounts": [ + { + "name": "registrar", + "isMut": false, + "isSigner": false + }, + { + "name": "voterWeightRecord", + "isMut": true, + "isSigner": false + }, + { + "name": "voterAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "compressionProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "voterWeightAction", + "type": { + "defined": "VoterWeightAction" + } + }, + { + "name": "params", + "type": { + "vec": { + "defined": "CompressedNftAsset" + } + } + } + ] + } + ], + "accounts": [ + { + "name": "nftVoteRecord", + "docs": [ + "NftVoteRecord exported to IDL without account_discriminator", + "TODO: Once we can support these accounts in Anchor via remaining_accounts then it should be possible to remove it" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "proposal", + "docs": [ + "Proposal which was voted on" + ], + "type": "publicKey" + }, + { + "name": "nftMint", + "docs": [ + "The mint of the NFT which was used for the vote" + ], + "type": "publicKey" + }, + { + "name": "governingTokenOwner", + "docs": [ + "The voter who casted this vote", + "It's a Realm member pubkey corresponding to TokenOwnerRecord.governing_token_owner" + ], + "type": "publicKey" + } + ] + } + }, + { + "name": "nftActionTicket", + "type": { + "kind": "struct", + "fields": [ + { + "name": "registrar", + "type": "publicKey" + }, + { + "name": "governingTokenOwner", + "type": "publicKey" + }, + { + "name": "nftMint", + "type": "publicKey" + }, + { + "name": "weight", + "type": "u64" + }, + { + "name": "expiry", + "type": { + "option": "u64" + } + } + ] + } + }, + { + "name": "maxVoterWeightRecord", + "docs": [ + "MaxVoterWeightRecord account as defined in spl-governance-addin-api", + "It's redefined here without account_discriminator for Anchor to treat it as native account", + "", + "The account is used as an api interface to provide max voting power to the governance program from external addin contracts" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "realm", + "docs": [ + "The Realm the MaxVoterWeightRecord belongs to" + ], + "type": "publicKey" + }, + { + "name": "governingTokenMint", + "docs": [ + "Governing Token Mint the MaxVoterWeightRecord is associated with", + "Note: The addin can take deposits of any tokens and is not restricted to the community or council tokens only" + ], + "type": "publicKey" + }, + { + "name": "maxVoterWeight", + "docs": [ + "Max voter weight", + "The max voter weight provided by the addin for the given realm and governing_token_mint" + ], + "type": "u64" + }, + { + "name": "maxVoterWeightExpiry", + "docs": [ + "The slot when the max voting weight expires", + "It should be set to None if the weight never expires", + "If the max vote weight decays with time, for example for time locked based weights, then the expiry must be set", + "As a pattern Revise instruction to update the max weight should be invoked before governance instruction within the same transaction", + "and the expiry set to the current slot to provide up to date weight" + ], + "type": { + "option": "u64" + } + }, + { + "name": "reserved", + "docs": [ + "Reserved space for future versions" + ], + "type": { + "array": [ + "u8", + 8 + ] + } + } + ] + } + }, + { + "name": "registrar", + "docs": [ + "Registrar which stores NFT voting configuration for the given Realm" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "governanceProgramId", + "docs": [ + "spl-governance program the Realm belongs to" + ], + "type": "publicKey" + }, + { + "name": "realm", + "docs": [ + "Realm of the Registrar" + ], + "type": "publicKey" + }, + { + "name": "governingTokenMint", + "docs": [ + "Governing token mint the Registrar is for", + "It can either be the Community or the Council mint of the Realm", + "When the plugin is used the mint is only used as identity of the governing power (voting population)", + "and the actual token of the mint is not used" + ], + "type": "publicKey" + }, + { + "name": "collectionConfigs", + "docs": [ + "MPL Collection used for voting" + ], + "type": { + "vec": { + "defined": "CollectionConfig" + } + } + }, + { + "name": "reserved", + "docs": [ + "Reserved for future upgrades" + ], + "type": { + "array": [ + "u8", + 128 + ] + } + } + ] + } + }, + { + "name": "voterWeightRecord", + "docs": [ + "VoterWeightRecord account as defined in spl-governance-addin-api", + "It's redefined here without account_discriminator for Anchor to treat it as native account", + "", + "The account is used as an api interface to provide voting power to the governance program from external addin contracts" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "realm", + "docs": [ + "The Realm the VoterWeightRecord belongs to" + ], + "type": "publicKey" + }, + { + "name": "governingTokenMint", + "docs": [ + "Governing Token Mint the VoterWeightRecord is associated with", + "Note: The addin can take deposits of any tokens and is not restricted to the community or council tokens only" + ], + "type": "publicKey" + }, + { + "name": "governingTokenOwner", + "docs": [ + "The owner of the governing token and voter", + "This is the actual owner (voter) and corresponds to TokenOwnerRecord.governing_token_owner" + ], + "type": "publicKey" + }, + { + "name": "voterWeight", + "docs": [ + "Voter's weight", + "The weight of the voter provided by the addin for the given realm, governing_token_mint and governing_token_owner (voter)" + ], + "type": "u64" + }, + { + "name": "voterWeightExpiry", + "docs": [ + "The slot when the voting weight expires", + "It should be set to None if the weight never expires", + "If the voter weight decays with time, for example for time locked based weights, then the expiry must be set", + "As a common pattern Revise instruction to update the weight should be invoked before governance instruction within the same transaction", + "and the expiry set to the current slot to provide up to date weight" + ], + "type": { + "option": "u64" + } + }, + { + "name": "weightAction", + "docs": [ + "The governance action the voter's weight pertains to", + "It allows to provided voter's weight specific to the particular action the weight is evaluated for", + "When the action is provided then the governance program asserts the executing action is the same as specified by the addin" + ], + "type": { + "option": { + "defined": "VoterWeightAction" + } + } + }, + { + "name": "weightActionTarget", + "docs": [ + "The target the voter's weight action pertains to", + "It allows to provided voter's weight specific to the target the weight is evaluated for", + "For example when addin supplies weight to vote on a particular proposal then it must specify the proposal as the action target", + "When the target is provided then the governance program asserts the target is the same as specified by the addin" + ], + "type": { + "option": "publicKey" + } + }, + { + "name": "reserved", + "docs": [ + "Reserved space for future versions" + ], + "type": { + "array": [ + "u8", + 8 + ] + } + } + ] + } + } + ], + "types": [ + { + "name": "Collection", + "type": { + "kind": "struct", + "fields": [ + { + "name": "verified", + "type": "bool" + }, + { + "name": "key", + "type": "publicKey" + } + ] + } + }, + { + "name": "Creator", + "type": { + "kind": "struct", + "fields": [ + { + "name": "address", + "type": "publicKey" + }, + { + "name": "verified", + "type": "bool" + }, + { + "name": "share", + "type": "u8" + } + ] + } + }, + { + "name": "CompressedNftAsset", + "type": { + "kind": "struct", + "fields": [ + { + "name": "name", + "type": "string" + }, + { + "name": "symbol", + "type": "string" + }, + { + "name": "uri", + "type": "string" + }, + { + "name": "collection", + "type": { + "option": { + "defined": "Collection" + } + } + }, + { + "name": "sellerFeeBasisPoints", + "type": "u16" + }, + { + "name": "primarySaleHappened", + "type": "bool" + }, + { + "name": "isMutable", + "type": "bool" + }, + { + "name": "editionNonce", + "type": { + "option": "u8" + } + }, + { + "name": "creators", + "type": { + "vec": { + "defined": "Creator" + } + } + }, + { + "name": "root", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "leafOwner", + "type": "publicKey" + }, + { + "name": "leafDelegate", + "type": "publicKey" + }, + { + "name": "index", + "type": "u32" + }, + { + "name": "nonce", + "type": "u64" + }, + { + "name": "proofLen", + "type": "u8" + } + ] + } + }, + { + "name": "CollectionConfig", + "docs": [ + "Configuration of an NFT collection used for governance power" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "collection", + "docs": [ + "The NFT collection used for governance" + ], + "type": "publicKey" + }, + { + "name": "size", + "docs": [ + "The size of the NFT collection used to calculate max voter weight", + "Note: At the moment the size is not captured on Metaplex accounts", + "and it has to be manually updated on the Registrar" + ], + "type": "u32" + }, + { + "name": "weight", + "docs": [ + "Governance power weight of the collection", + "Each NFT in the collection has governance power = 1 * weight", + "Note: The weight is scaled accordingly to the governing_token_mint decimals", + "Ex: if the the mint has 2 decimal places then weight of 1 should be stored as 100" + ], + "type": "u64" + }, + { + "name": "reserved", + "docs": [ + "Reserved for future upgrades" + ], + "type": { + "array": [ + "u8", + 8 + ] + } + } + ] + } + }, + { + "name": "VoterWeightAction", + "docs": [ + "VoterWeightAction enum as defined in spl-governance-addin-api", + "It's redefined here for Anchor to export it to IDL" + ], + "type": { + "kind": "enum", + "variants": [ + { + "name": "CastVote" + }, + { + "name": "CommentProposal" + }, + { + "name": "CreateGovernance" + }, + { + "name": "CreateProposal" + }, + { + "name": "SignOffProposal" + } + ] + } + } + ], + "errors": [ + { + "code": 6000, + "name": "InvalidRealmAuthority", + "msg": "Invalid Realm Authority" + }, + { + "code": 6001, + "name": "InvalidRealmForRegistrar", + "msg": "Invalid Realm for Registrar" + }, + { + "code": 6002, + "name": "InvalidCollectionSize", + "msg": "Invalid Collection Size" + }, + { + "code": 6003, + "name": "InvalidMaxVoterWeightRecordRealm", + "msg": "Invalid MaxVoterWeightRecord Realm" + }, + { + "code": 6004, + "name": "InvalidMaxVoterWeightRecordMint", + "msg": "Invalid MaxVoterWeightRecord Mint" + }, + { + "code": 6005, + "name": "CastVoteIsNotAllowed", + "msg": "CastVote Is Not Allowed" + }, + { + "code": 6006, + "name": "InvalidVoterWeightRecordRealm", + "msg": "Invalid VoterWeightRecord Realm" + }, + { + "code": 6007, + "name": "InvalidVoterWeightRecordMint", + "msg": "Invalid VoterWeightRecord Mint" + }, + { + "code": 6008, + "name": "InvalidTokenOwnerForVoterWeightRecord", + "msg": "Invalid TokenOwner for VoterWeightRecord" + }, + { + "code": 6009, + "name": "CollectionMustBeVerified", + "msg": "Collection must be verified" + }, + { + "code": 6010, + "name": "VoterDoesNotOwnNft", + "msg": "Voter does not own NFT" + }, + { + "code": 6011, + "name": "CollectionNotFound", + "msg": "Collection not found" + }, + { + "code": 6012, + "name": "MissingMetadataCollection", + "msg": "Missing Metadata collection" + }, + { + "code": 6013, + "name": "TokenMetadataDoesNotMatch", + "msg": "Token Metadata doesn't match" + }, + { + "code": 6014, + "name": "InvalidAccountOwner", + "msg": "Invalid account owner" + }, + { + "code": 6015, + "name": "InvalidTokenMetadataAccount", + "msg": "Invalid token metadata account" + }, + { + "code": 6016, + "name": "DuplicatedNftDetected", + "msg": "Duplicated NFT detected" + }, + { + "code": 6017, + "name": "InvalidNftAmount", + "msg": "Invalid NFT amount" + }, + { + "code": 6018, + "name": "NftAlreadyVoted", + "msg": "NFT already voted" + }, + { + "code": 6019, + "name": "InvalidProposalForNftVoteRecord", + "msg": "Invalid Proposal for NftVoteRecord" + }, + { + "code": 6020, + "name": "InvalidTokenOwnerForNftVoteRecord", + "msg": "Invalid TokenOwner for NftVoteRecord" + }, + { + "code": 6021, + "name": "VoteRecordMustBeWithdrawn", + "msg": "VoteRecord must be withdrawn" + }, + { + "code": 6022, + "name": "InvalidVoteRecordForNftVoteRecord", + "msg": "Invalid VoteRecord for NftVoteRecord" + }, + { + "code": 6023, + "name": "VoterWeightRecordMustBeExpired", + "msg": "VoterWeightRecord must be expired" + }, + { + "code": 6024, + "name": "InvalidInstruction", + "msg": "Invalid instruction" + }, + { + "code": 6025, + "name": "InvalidVoteRecordAccount", + "msg": "Invalid Vote Record Account" + }, + { + "code": 6026, + "name": "GoverningTokenOwnerOrDelegateMustSign", + "msg": "Governance Token Owner Or Delegate Must Sign" + }, + { + "code": 6027, + "name": "NftFailedVerification", + "msg": "NFT Failed Verification" + }, + { + "code": 6028, + "name": "NftTicketExpired", + "msg": "Nft Ticket Expired" + }, + { + "code": 6029, + "name": "InvalidNftTicket", + "msg": "Voter With Invalid Ticket" + } + ] +}; + +export const IDLV2: NftVoterV2 = { + "version": "0.2.3", + "name": "nft_voter", + "instructions": [ + { + "name": "createRegistrar", + "accounts": [ + { + "name": "registrar", + "isMut": true, + "isSigner": false, + "docs": [ + "The NFT voting Registrar", + "There can only be a single registrar per governance Realm and governing mint of the Realm" + ] + }, + { + "name": "governanceProgramId", + "isMut": false, + "isSigner": false, + "docs": [ + "The program id of the spl-governance program the realm belongs to" + ] + }, + { + "name": "realm", + "isMut": false, + "isSigner": false, + "docs": [ + "An spl-governance Realm", + "", + "Realm is validated in the instruction:", + "- Realm is owned by the governance_program_id", + "- governing_token_mint must be the community or council mint", + "- realm_authority is realm.authority" + ] + }, + { + "name": "governingTokenMint", + "isMut": false, + "isSigner": false, + "docs": [ + "Either the realm community mint or the council mint.", + "It must match Realm.community_mint or Realm.config.council_mint", + "", + "Note: Once the NFT plugin is enabled the governing_token_mint is used only as identity", + "for the voting population and the tokens of that are no longer used" + ] + }, + { + "name": "realmAuthority", + "isMut": false, + "isSigner": true, + "docs": [ + "realm_authority must sign and match Realm.authority" + ] + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "maxCollections", + "type": "u8" + } + ] + }, + { + "name": "createVoterWeightRecord", + "accounts": [ + { + "name": "voterWeightRecord", + "isMut": true, + "isSigner": false + }, + { + "name": "governanceProgramId", + "isMut": false, + "isSigner": false, + "docs": [ + "The program id of the spl-governance program the realm belongs to" + ] + }, + { + "name": "realm", + "isMut": false, + "isSigner": false + }, + { + "name": "realmGoverningTokenMint", + "isMut": false, + "isSigner": false, + "docs": [ + "Either the realm community mint or the council mint." + ] + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "governingTokenOwner", + "type": "publicKey" + } + ] + }, + { + "name": "createMaxVoterWeightRecord", + "accounts": [ + { + "name": "maxVoterWeightRecord", + "isMut": true, + "isSigner": false + }, + { + "name": "governanceProgramId", + "isMut": false, + "isSigner": false, + "docs": [ + "The program id of the spl-governance program the realm belongs to" + ] + }, + { + "name": "realm", + "isMut": false, + "isSigner": false + }, + { + "name": "realmGoverningTokenMint", + "isMut": false, + "isSigner": false, + "docs": [ + "Either the realm community mint or the council mint." + ] + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "updateVoterWeightRecord", + "accounts": [ + { + "name": "registrar", + "isMut": false, + "isSigner": false, + "docs": [ + "The NFT voting Registrar" + ] + }, + { + "name": "voterWeightRecord", + "isMut": true, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + } + ], + "args": [ + { + "name": "voterWeightAction", + "type": { + "defined": "VoterWeightAction" + } + } + ] + }, + { + "name": "relinquishNftVote", + "accounts": [ + { + "name": "registrar", + "isMut": false, + "isSigner": false, + "docs": [ + "The NFT voting Registrar" + ] + }, + { + "name": "voterWeightRecord", + "isMut": true, + "isSigner": false + }, + { + "name": "governance", + "isMut": false, + "isSigner": false, + "docs": [ + "Governance account the Proposal is for" + ] + }, + { + "name": "proposal", + "isMut": false, + "isSigner": false + }, + { + "name": "voterTokenOwnerRecord", + "isMut": false, + "isSigner": false, + "docs": [ + "TokenOwnerRecord of the voter who cast the original vote" + ] + }, + { + "name": "voterAuthority", + "isMut": false, + "isSigner": true, + "docs": [ + "Authority of the voter who cast the original vote", + "It can be either governing_token_owner or its delegate and must sign this instruction" + ] + }, + { + "name": "voteRecord", + "isMut": false, + "isSigner": false, + "docs": [ + "The account is used to validate that it doesn't exist and if it doesn't then Anchor owner check throws error", + "The check is disabled here and performed inside the instruction", + "#[account(owner = registrar.governance_program_id)]" + ] + }, + { + "name": "beneficiary", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "configureCollection", + "accounts": [ + { + "name": "registrar", + "isMut": true, + "isSigner": false, + "docs": [ + "Registrar for which we configure this Collection" + ] + }, + { + "name": "realm", + "isMut": false, + "isSigner": false + }, + { + "name": "realmAuthority", + "isMut": false, + "isSigner": true, + "docs": [ + "Authority of the Realm must sign and match Realm.authority" + ] + }, + { + "name": "collection", + "isMut": false, + "isSigner": false + }, + { + "name": "maxVoterWeightRecord", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "weight", + "type": "u64" + }, + { + "name": "size", + "type": "u32" + } + ] + }, + { + "name": "castNftVote", + "accounts": [ + { + "name": "registrar", + "isMut": false, + "isSigner": false, + "docs": [ + "The NFT voting registrar" + ] + }, + { + "name": "voterWeightRecord", + "isMut": true, + "isSigner": false + }, + { + "name": "voterTokenOwnerRecord", + "isMut": false, + "isSigner": false, + "docs": [ + "TokenOwnerRecord of the voter who casts the vote", + "/// CHECK: Owned by spl-governance instance specified in registrar.governance_program_id" + ] + }, + { + "name": "voterAuthority", + "isMut": false, + "isSigner": true, + "docs": [ + "Authority of the voter who casts the vote", + "It can be either governing_token_owner or its delegate and must sign this instruction" + ] + }, + { + "name": "payer", + "isMut": true, + "isSigner": true, + "docs": [ + "The account which pays for the transaction" + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "proposal", + "type": "publicKey" + } + ] + }, + { + "name": "createNftActionTicket", + "accounts": [ + { + "name": "registrar", + "isMut": false, + "isSigner": false + }, + { + "name": "voterWeightRecord", + "isMut": true, + "isSigner": false + }, + { + "name": "voterAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "voterWeightAction", + "type": { + "defined": "VoterWeightAction" + } + } + ] + }, + { + "name": "createCnftActionTicket", + "accounts": [ + { + "name": "registrar", + "isMut": false, + "isSigner": false + }, + { + "name": "voterWeightRecord", + "isMut": true, + "isSigner": false + }, + { + "name": "voterAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "compressionProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "voterWeightAction", + "type": { + "defined": "VoterWeightAction" + } + }, + { + "name": "params", + "type": { + "vec": { + "defined": "CompressedNftAsset" + } + } + } + ] + } + ], + "accounts": [ + { + "name": "nftVoteRecord", + "docs": [ + "NftVoteRecord exported to IDL without account_discriminator", + "TODO: Once we can support these accounts in Anchor via remaining_accounts then it should be possible to remove it" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "proposal", + "docs": [ + "Proposal which was voted on" + ], + "type": "publicKey" + }, + { + "name": "nftMint", + "docs": [ + "The mint of the NFT which was used for the vote" + ], + "type": "publicKey" + }, + { + "name": "governingTokenOwner", + "docs": [ + "The voter who casted this vote", + "It's a Realm member pubkey corresponding to TokenOwnerRecord.governing_token_owner" + ], + "type": "publicKey" + } + ] + } + }, + { + "name": "nftActionTicket", + "type": { + "kind": "struct", + "fields": [ + { + "name": "registrar", + "type": "publicKey" + }, + { + "name": "governingTokenOwner", + "type": "publicKey" + }, + { + "name": "nftMint", + "type": "publicKey" + }, + { + "name": "weight", + "type": "u64" + }, + { + "name": "expiry", + "type": { + "option": "u64" + } + } + ] + } + }, + { + "name": "maxVoterWeightRecord", + "docs": [ + "MaxVoterWeightRecord account as defined in spl-governance-addin-api", + "It's redefined here without account_discriminator for Anchor to treat it as native account", + "", + "The account is used as an api interface to provide max voting power to the governance program from external addin contracts" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "realm", + "docs": [ + "The Realm the MaxVoterWeightRecord belongs to" + ], + "type": "publicKey" + }, + { + "name": "governingTokenMint", + "docs": [ + "Governing Token Mint the MaxVoterWeightRecord is associated with", + "Note: The addin can take deposits of any tokens and is not restricted to the community or council tokens only" + ], + "type": "publicKey" + }, + { + "name": "maxVoterWeight", + "docs": [ + "Max voter weight", + "The max voter weight provided by the addin for the given realm and governing_token_mint" + ], + "type": "u64" + }, + { + "name": "maxVoterWeightExpiry", + "docs": [ + "The slot when the max voting weight expires", + "It should be set to None if the weight never expires", + "If the max vote weight decays with time, for example for time locked based weights, then the expiry must be set", + "As a pattern Revise instruction to update the max weight should be invoked before governance instruction within the same transaction", + "and the expiry set to the current slot to provide up to date weight" + ], + "type": { + "option": "u64" + } + }, + { + "name": "reserved", + "docs": [ + "Reserved space for future versions" + ], + "type": { + "array": [ + "u8", + 8 + ] + } + } + ] + } + }, + { + "name": "registrar", + "docs": [ + "Registrar which stores NFT voting configuration for the given Realm" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "governanceProgramId", + "docs": [ + "spl-governance program the Realm belongs to" + ], + "type": "publicKey" + }, + { + "name": "realm", + "docs": [ + "Realm of the Registrar" + ], + "type": "publicKey" + }, + { + "name": "governingTokenMint", + "docs": [ + "Governing token mint the Registrar is for", + "It can either be the Community or the Council mint of the Realm", + "When the plugin is used the mint is only used as identity of the governing power (voting population)", + "and the actual token of the mint is not used" + ], + "type": "publicKey" + }, + { + "name": "collectionConfigs", + "docs": [ + "MPL Collection used for voting" + ], + "type": { + "vec": { + "defined": "CollectionConfig" + } + } + }, + { + "name": "reserved", + "docs": [ + "Reserved for future upgrades" + ], + "type": { + "array": [ + "u8", + 128 + ] + } + } + ] + } + }, + { + "name": "voterWeightRecord", + "docs": [ + "VoterWeightRecord account as defined in spl-governance-addin-api", + "It's redefined here without account_discriminator for Anchor to treat it as native account", + "", + "The account is used as an api interface to provide voting power to the governance program from external addin contracts" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "realm", + "docs": [ + "The Realm the VoterWeightRecord belongs to" + ], + "type": "publicKey" + }, + { + "name": "governingTokenMint", + "docs": [ + "Governing Token Mint the VoterWeightRecord is associated with", + "Note: The addin can take deposits of any tokens and is not restricted to the community or council tokens only" + ], + "type": "publicKey" + }, + { + "name": "governingTokenOwner", + "docs": [ + "The owner of the governing token and voter", + "This is the actual owner (voter) and corresponds to TokenOwnerRecord.governing_token_owner" + ], + "type": "publicKey" + }, + { + "name": "voterWeight", + "docs": [ + "Voter's weight", + "The weight of the voter provided by the addin for the given realm, governing_token_mint and governing_token_owner (voter)" + ], + "type": "u64" + }, + { + "name": "voterWeightExpiry", + "docs": [ + "The slot when the voting weight expires", + "It should be set to None if the weight never expires", + "If the voter weight decays with time, for example for time locked based weights, then the expiry must be set", + "As a common pattern Revise instruction to update the weight should be invoked before governance instruction within the same transaction", + "and the expiry set to the current slot to provide up to date weight" + ], + "type": { + "option": "u64" + } + }, + { + "name": "weightAction", + "docs": [ + "The governance action the voter's weight pertains to", + "It allows to provided voter's weight specific to the particular action the weight is evaluated for", + "When the action is provided then the governance program asserts the executing action is the same as specified by the addin" + ], + "type": { + "option": { + "defined": "VoterWeightAction" + } + } + }, + { + "name": "weightActionTarget", + "docs": [ + "The target the voter's weight action pertains to", + "It allows to provided voter's weight specific to the target the weight is evaluated for", + "For example when addin supplies weight to vote on a particular proposal then it must specify the proposal as the action target", + "When the target is provided then the governance program asserts the target is the same as specified by the addin" + ], + "type": { + "option": "publicKey" + } + }, + { + "name": "reserved", + "docs": [ + "Reserved space for future versions" + ], + "type": { + "array": [ + "u8", + 8 + ] + } + } + ] + } + } + ], + "types": [ + { + "name": "Collection", + "type": { + "kind": "struct", + "fields": [ + { + "name": "verified", + "type": "bool" + }, + { + "name": "key", + "type": "publicKey" + } + ] + } + }, + { + "name": "Creator", + "type": { + "kind": "struct", + "fields": [ + { + "name": "address", + "type": "publicKey" + }, + { + "name": "verified", + "type": "bool" + }, + { + "name": "share", + "type": "u8" + } + ] + } + }, + { + "name": "CompressedNftAsset", + "type": { + "kind": "struct", + "fields": [ + { + "name": "name", + "type": "string" + }, + { + "name": "symbol", + "type": "string" + }, + { + "name": "uri", + "type": "string" + }, + { + "name": "collection", + "type": { + "option": { + "defined": "Collection" + } + } + }, + { + "name": "sellerFeeBasisPoints", + "type": "u16" + }, + { + "name": "primarySaleHappened", + "type": "bool" + }, + { + "name": "isMutable", + "type": "bool" + }, + { + "name": "editionNonce", + "type": { + "option": "u8" + } + }, + { + "name": "creators", + "type": { + "vec": { + "defined": "Creator" + } + } + }, + { + "name": "root", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "leafOwner", + "type": "publicKey" + }, + { + "name": "leafDelegate", + "type": "publicKey" + }, + { + "name": "index", + "type": "u32" + }, + { + "name": "nonce", + "type": "u64" + }, + { + "name": "proofLen", + "type": "u8" + } + ] + } + }, + { + "name": "CollectionConfig", + "docs": [ + "Configuration of an NFT collection used for governance power" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "collection", + "docs": [ + "The NFT collection used for governance" + ], + "type": "publicKey" + }, + { + "name": "size", + "docs": [ + "The size of the NFT collection used to calculate max voter weight", + "Note: At the moment the size is not captured on Metaplex accounts", + "and it has to be manually updated on the Registrar" + ], + "type": "u32" + }, + { + "name": "weight", + "docs": [ + "Governance power weight of the collection", + "Each NFT in the collection has governance power = 1 * weight", + "Note: The weight is scaled accordingly to the governing_token_mint decimals", + "Ex: if the the mint has 2 decimal places then weight of 1 should be stored as 100" + ], + "type": "u64" + }, + { + "name": "reserved", + "docs": [ + "Reserved for future upgrades" + ], + "type": { + "array": [ + "u8", + 8 + ] + } + } + ] + } + }, + { + "name": "VoterWeightAction", + "docs": [ + "VoterWeightAction enum as defined in spl-governance-addin-api", + "It's redefined here for Anchor to export it to IDL" + ], + "type": { + "kind": "enum", + "variants": [ + { + "name": "CastVote" + }, + { + "name": "CommentProposal" + }, + { + "name": "CreateGovernance" + }, + { + "name": "CreateProposal" + }, + { + "name": "SignOffProposal" + } + ] + } + } + ], + "errors": [ + { + "code": 6000, + "name": "InvalidRealmAuthority", + "msg": "Invalid Realm Authority" + }, + { + "code": 6001, + "name": "InvalidRealmForRegistrar", + "msg": "Invalid Realm for Registrar" + }, + { + "code": 6002, + "name": "InvalidCollectionSize", + "msg": "Invalid Collection Size" + }, + { + "code": 6003, + "name": "InvalidMaxVoterWeightRecordRealm", + "msg": "Invalid MaxVoterWeightRecord Realm" + }, + { + "code": 6004, + "name": "InvalidMaxVoterWeightRecordMint", + "msg": "Invalid MaxVoterWeightRecord Mint" + }, + { + "code": 6005, + "name": "CastVoteIsNotAllowed", + "msg": "CastVote Is Not Allowed" + }, + { + "code": 6006, + "name": "InvalidVoterWeightRecordRealm", + "msg": "Invalid VoterWeightRecord Realm" + }, + { + "code": 6007, + "name": "InvalidVoterWeightRecordMint", + "msg": "Invalid VoterWeightRecord Mint" + }, + { + "code": 6008, + "name": "InvalidTokenOwnerForVoterWeightRecord", + "msg": "Invalid TokenOwner for VoterWeightRecord" + }, + { + "code": 6009, + "name": "CollectionMustBeVerified", + "msg": "Collection must be verified" + }, + { + "code": 6010, + "name": "VoterDoesNotOwnNft", + "msg": "Voter does not own NFT" + }, + { + "code": 6011, + "name": "CollectionNotFound", + "msg": "Collection not found" + }, + { + "code": 6012, + "name": "MissingMetadataCollection", + "msg": "Missing Metadata collection" + }, + { + "code": 6013, + "name": "TokenMetadataDoesNotMatch", + "msg": "Token Metadata doesn't match" + }, + { + "code": 6014, + "name": "InvalidAccountOwner", + "msg": "Invalid account owner" + }, + { + "code": 6015, + "name": "InvalidTokenMetadataAccount", + "msg": "Invalid token metadata account" + }, + { + "code": 6016, + "name": "DuplicatedNftDetected", + "msg": "Duplicated NFT detected" + }, + { + "code": 6017, + "name": "InvalidNftAmount", + "msg": "Invalid NFT amount" + }, + { + "code": 6018, + "name": "NftAlreadyVoted", + "msg": "NFT already voted" + }, + { + "code": 6019, + "name": "InvalidProposalForNftVoteRecord", + "msg": "Invalid Proposal for NftVoteRecord" + }, + { + "code": 6020, + "name": "InvalidTokenOwnerForNftVoteRecord", + "msg": "Invalid TokenOwner for NftVoteRecord" + }, + { + "code": 6021, + "name": "VoteRecordMustBeWithdrawn", + "msg": "VoteRecord must be withdrawn" + }, + { + "code": 6022, + "name": "InvalidVoteRecordForNftVoteRecord", + "msg": "Invalid VoteRecord for NftVoteRecord" + }, + { + "code": 6023, + "name": "VoterWeightRecordMustBeExpired", + "msg": "VoterWeightRecord must be expired" + }, + { + "code": 6024, + "name": "InvalidInstruction", + "msg": "Invalid instruction" + }, + { + "code": 6025, + "name": "InvalidVoteRecordAccount", + "msg": "Invalid Vote Record Account" + }, + { + "code": 6026, + "name": "GoverningTokenOwnerOrDelegateMustSign", + "msg": "Governance Token Owner Or Delegate Must Sign" + }, + { + "code": 6027, + "name": "NftFailedVerification", + "msg": "NFT Failed Verification" + }, + { + "code": 6028, + "name": "NftTicketExpired", + "msg": "Nft Ticket Expired" + }, + { + "code": 6029, + "name": "InvalidNftTicket", + "msg": "Voter With Invalid Ticket" + } + ] +}; diff --git a/models/proposal/buildTopVoters.ts b/models/proposal/buildTopVoters.ts index c9ae35ea05..8ddaa571b1 100644 --- a/models/proposal/buildTopVoters.ts +++ b/models/proposal/buildTopVoters.ts @@ -2,8 +2,6 @@ import { ProgramAccount, VoteRecord, TokenOwnerRecord, - Realm, - Proposal, VoteKind, } from '@solana/spl-governance' import { MintInfo } from '@solana/spl-token' @@ -11,8 +9,6 @@ import BN from 'bn.js' import { PublicKey } from '@solana/web3.js' import { BigNumber } from 'bignumber.js' -import { calculateMaxVoteScore } from '@models/proposal/calulateMaxVoteScore' - export enum VoteType { No, Undecided, @@ -51,13 +47,10 @@ const ZERO = new BN(0) export function buildTopVoters( voteRecords: ProgramAccount[], tokenOwnerRecords: ProgramAccount[], - realm: ProgramAccount, - proposal: ProgramAccount, governingTokenMint: MintInfo, - undecidedVoterWeightByWallets: { [walletPk: string]: BN } + undecidedVoterWeightByWallets: { [walletPk: string]: BN }, + maxVote: BN ): VoterDisplayData[] { - const maxVote = calculateMaxVoteScore(realm, proposal, governingTokenMint) - const electoralVotes = voteRecords.filter( (x) => x.account.vote?.voteType !== VoteKind.Veto ) diff --git a/pages/dao/[symbol]/members/Members.tsx b/pages/dao/[symbol]/members/Members.tsx index a463c77118..ef230c195b 100644 --- a/pages/dao/[symbol]/members/Members.tsx +++ b/pages/dao/[symbol]/members/Members.tsx @@ -24,6 +24,7 @@ const Members = () => { } = useRealm() const pagination = useRef<{ setPage: (val) => void }>(null) const membersPerPage = 10 + const { data: activeMembers } = useMembersQuery() const wallet = useWalletOnePointOh() const connected = !!wallet?.connected @@ -192,7 +193,12 @@ const Members = () => {
- {activeMember ? : null} + {activeMember ? ( + + ) : null}
{openAddMemberModal && ( diff --git a/pages/dao/[symbol]/members/index.tsx b/pages/dao/[symbol]/members/index.tsx index 3d37704646..27f1b06c3d 100644 --- a/pages/dao/[symbol]/members/index.tsx +++ b/pages/dao/[symbol]/members/index.tsx @@ -1,10 +1,16 @@ +import { NFT_PLUGINS_PKS } from '@constants/plugins' import Members from './Members' import { useRealmConfigQuery } from '@hooks/queries/realmConfig' const MembersPage = () => { const config = useRealmConfigQuery().data?.result + const currentPluginPk = config?.account.communityTokenConfig.voterWeightAddin + const isNftMode = + (currentPluginPk && + NFT_PLUGINS_PKS.includes(currentPluginPk?.toBase58())) || + false return (
- {!config?.account.communityTokenConfig.voterWeightAddin ? ( + {!config?.account.communityTokenConfig.voterWeightAddin || isNftMode ? ( ) : null}
diff --git a/pages/dao/[symbol]/proposal/[pk]/explore.tsx b/pages/dao/[symbol]/proposal/[pk]/explore.tsx index 9fcebe8b62..ed625a4d03 100644 --- a/pages/dao/[symbol]/proposal/[pk]/explore.tsx +++ b/pages/dao/[symbol]/proposal/[pk]/explore.tsx @@ -16,6 +16,9 @@ import { useRouteProposalQuery } from '@hooks/queries/proposal' import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext' import { VoteType } from '@solana/spl-governance' import MultiChoiceVotes from '@components/MultiChoiceVotes' +import { useRealmConfigQuery } from '@hooks/queries/realmConfig' +import ProposalVoterNftChart from '@components/ProposalVoterNftChart' +import { NFT_PLUGINS_PKS } from '@constants/plugins' export default function Explore() { const proposal = useRouteProposalQuery().data?.result @@ -26,13 +29,17 @@ export default function Explore() { const signatories = useSignatories(proposal) const router = useRouter() + const config = useRealmConfigQuery().data?.result + const currentPluginPk = config?.account.communityTokenConfig.voterWeightAddin + const isNftMode = + currentPluginPk && NFT_PLUGINS_PKS.includes(currentPluginPk?.toBase58()) + const endpoint = connection.endpoint const handleExploreBackClick = () => { const newPath = router.asPath.replace(/\/explore$/, '') router.push(newPath) } - const isMulti = proposal?.account.voteType !== VoteType.SINGLE_CHOICE return ( @@ -58,19 +65,33 @@ export default function Explore() {

{proposal?.account.name}

-

Top Voters

+
+

Top Voters

+
setHighlighted(undefined)} > - +
+ + {/* when hovering over a top voter, ProposalVoterNftChart shows he/her NFTs when isNftMode */} + x.key === highlighted)?.voteType + : undefined + } + /> +
- { - isMulti ? -
-

Vote Result

- + {isMulti ? ( +
+

Vote Result

+
- : + ) : ( - } + )} , + walletPk: PublicKey, + registrar: PublicKey, + proposal: PublicKey, + tokenOwnerRecord: PublicKey, + voterWeightPk: PublicKey, + votingNfts: DasNftObject[], + nftVoteRecordsFiltered: NftVoteRecord[] +) => { + console.log('getCastNftVoteInstruction') + const clientProgramId = program.programId + const remainingAccounts: AccountData[] = [] + for (let i = 0; i < votingNfts.length; i++) { + const nft = votingNfts[i] + // const tokenAccount = await nft.getAssociatedTokenAccount() + const tokenAccount = await getAssociatedTokenAddress( + new PublicKey(nft.id), + walletPk, + true + ) + const metadata = await fetchNFTbyMint( + program.provider.connection, + new PublicKey(nft.id) + ) + const { nftVoteRecord } = await getNftVoteRecordProgramAddress( + proposal, + nft.id, + clientProgramId + ) + if ( + !nftVoteRecordsFiltered.find( + (x) => x.publicKey.toBase58() === nftVoteRecord.toBase58() + ) + ) + remainingAccounts.push( + new AccountData(tokenAccount), + new AccountData(metadata?.result?.metadataAddress || ''), + new AccountData(nftVoteRecord, false, true) + ) + } + + const castNftVoteIxs: TransactionInstruction[] = [] + //1 nft is 3 accounts + const nftChunks = chunks(remainingAccounts, 12) + for (const chunk of [...nftChunks]) { + castNftVoteIxs.push( + await program.methods + .castNftVote(proposal) + .accounts({ + registrar, + voterWeightRecord: voterWeightPk, + voterTokenOwnerRecord: tokenOwnerRecord, + voterAuthority: walletPk, + payer: walletPk, + systemProgram: SYSTEM_PROGRAM_ID, + }) + .remainingAccounts(chunk) + .instruction() + ) + } + return castNftVoteIxs +} + +export const getCastNftVoteInstructionV2 = async ( + program: Program, + walletPk: PublicKey, + registrar: PublicKey, + proposal: PublicKey, + tokenOwnerRecord: PublicKey, + voterWeightPk: PublicKey, + votingNfts: DasNftObject[], + nftVoteRecordsFiltered: NftVoteRecord[] +) => { + console.log('getCastNftVoteInstructionV2') + const clientProgramId = program.programId + const castNftVoteTicketIxs: TransactionInstruction[] = [] + const castVoteRemainingAccounts: AccountData[] = [] + const type: UpdateVoterWeightRecordTypes = 'castVote' + const ticketType = `nft-${type}-ticket` + // create nft weight records for all nfts + const nfts = votingNfts.filter((x) => !x.compression.compressed) + const nftRemainingAccounts: AccountData[] = [] + for (const nft of nfts) { + const { nftVoteRecord } = await getNftVoteRecordProgramAddress( + proposal, + nft.id, + clientProgramId + ) + if ( + !nftVoteRecordsFiltered.find( + (x) => x.publicKey.toBase58() === nftVoteRecord.toBase58() + ) + ) { + const { nftActionTicket } = getNftActionTicketProgramAddress( + ticketType, + registrar, + walletPk, + nft.id, + clientProgramId + ) + + const tokenAccount = await getAssociatedTokenAddress( + new PublicKey(nft.id), + walletPk, + true + ) + const metadata = await fetchNFTbyMint( + program.provider.connection, + new PublicKey(nft.id) + ) + + nftRemainingAccounts.push( + new AccountData(tokenAccount), + new AccountData(metadata?.result?.metadataAddress || ''), + new AccountData(nftActionTicket, false, true) + ) + + castVoteRemainingAccounts.push( + new AccountData(nftActionTicket, false, true), + new AccountData(nftVoteRecord, false, true) + ) + } + } + + const createNftVoteTicketChunks = chunks(nftRemainingAccounts, 15) + for (const chunk of [...createNftVoteTicketChunks]) { + castNftVoteTicketIxs.push( + await program.methods + .createNftActionTicket({ [type]: {} }) + .accounts({ + registrar, + voterWeightRecord: voterWeightPk, + voterAuthority: walletPk, + payer: walletPk, + systemProgram: SYSTEM_PROGRAM_ID, + }) + .remainingAccounts(chunk) + .instruction() + ) + } + + // create nft weight record for all compressed nfts + const cnfts = votingNfts.filter((x) => x.compression.compressed) + for (const cnft of cnfts) { + const { nftVoteRecord } = await getNftVoteRecordProgramAddress( + proposal, + cnft.id, + clientProgramId + ) + if ( + !nftVoteRecordsFiltered.find( + (x) => x.publicKey.toBase58() === nftVoteRecord.toBase58() + ) + ) { + const { nftActionTicket } = getNftActionTicketProgramAddress( + ticketType, + registrar, + walletPk, + cnft.id, + clientProgramId + ) + + const { param, additionalAccounts } = await getCnftParamAndProof( + program.provider.connection, + cnft + ) + + const instruction = await program.methods + .createCnftActionTicket({ [type]: {} }, [param]) + .accounts({ + registrar, + voterWeightRecord: voterWeightPk, + payer: walletPk, + compressionProgram: ACCOUNT_COMPACTION_PROGRAM_ID, + systemProgram: SYSTEM_PROGRAM_ID, + }) + .remainingAccounts([ + ...additionalAccounts.map((x) => new AccountData(x)), + new AccountData(nftActionTicket, false, true), + ]) + .instruction() + castNftVoteTicketIxs.push(instruction) + + castVoteRemainingAccounts.push( + new AccountData(nftActionTicket, false, true), + new AccountData(nftVoteRecord, false, true) + ) + } + } + + const castNftVoteIxs: TransactionInstruction[] = [] + const castVoteRemainingAccountsChunks = chunks(castVoteRemainingAccounts, 12) + for (const chunk of [...castVoteRemainingAccountsChunks]) { + castNftVoteIxs.push( + await program.methods + .castNftVote(proposal) + .accounts({ + registrar, + voterWeightRecord: voterWeightPk, + voterTokenOwnerRecord: tokenOwnerRecord, + voterAuthority: walletPk, + payer: walletPk, + systemProgram: SYSTEM_PROGRAM_ID, + }) + .remainingAccounts(chunk) + .instruction() + ) + } + + return { castNftVoteTicketIxs, castNftVoteIxs } +} diff --git a/utils/instructions/NftVoter/updateVoterWeight.ts b/utils/instructions/NftVoter/updateVoterWeight.ts new file mode 100644 index 0000000000..9a11626b47 --- /dev/null +++ b/utils/instructions/NftVoter/updateVoterWeight.ts @@ -0,0 +1,163 @@ +import { getAssociatedTokenAddress } from '@blockworks-foundation/mango-v4' +import { DasNftObject } from '@hooks/queries/digitalAssets' +import { fetchNFTbyMint } from '@hooks/queries/nft' +import { PublicKey, TransactionInstruction } from '@solana/web3.js' +import { chunks } from '@utils/helpers' +import { getNftActionTicketProgramAddress } from 'NftVotePlugin/accounts' +import { PROGRAM_ID as ACCOUNT_COMPACTION_PROGRAM_ID } from '@solana/spl-account-compression' +import { SYSTEM_PROGRAM_ID } from '@solana/spl-governance' +import { NftVoter } from 'idls/nft_voter' +import { NftVoterV2 } from 'idls/nft_voter_v2' +import { Program } from '@project-serum/anchor' +import { + AccountData, + UpdateVoterWeightRecordTypes, +} from '@utils/uiTypes/VotePlugin' +import { getCnftParamAndProof } from 'NftVotePlugin/getCnftParamAndProof' + +export const getUpdateVoterWeightRecordInstruction = async ( + program: Program, + walletPk: PublicKey, + registrar: PublicKey, + voterWeightPk: PublicKey, + votingNfts: DasNftObject[], + type: UpdateVoterWeightRecordTypes +) => { + console.log('getUpdateVoterWeightRecordInstruction') + const remainingAccounts: AccountData[] = [] + for (let i = 0; i < votingNfts.length; i++) { + const nft = votingNfts[i] + // const tokenAccount = await nft.getAssociatedTokenAccount() + const tokenAccount = await getAssociatedTokenAddress( + new PublicKey(nft.id), + walletPk, + true + ) + + const metadata = await fetchNFTbyMint( + program.provider.connection, + new PublicKey(nft.id) + ) + + remainingAccounts.push( + new AccountData(tokenAccount), + new AccountData(metadata?.result?.metadataAddress || '') + ) + } + const updateVoterWeightRecordIx = await program.methods + .updateVoterWeightRecord({ [type]: {} }) + .accounts({ + registrar: registrar, + voterWeightRecord: voterWeightPk, + }) + .remainingAccounts(remainingAccounts.slice(0, 10)) + .instruction() + return updateVoterWeightRecordIx +} + +export const getUpdateVoterWeightRecordInstructionV2 = async ( + program: Program, + walletPk: PublicKey, + registrar: PublicKey, + voterWeightPk: PublicKey, + votingNfts: DasNftObject[], + type: UpdateVoterWeightRecordTypes +) => { + console.log('getUpdateVoterWeightRecordInstructionV2') + const createNftTicketIxs: TransactionInstruction[] = [] + const ticketType = `nft-${type}-ticket` + const firstTenNfts = votingNfts.slice(0, 10) + const nftActionTicketAccounts: AccountData[] = [] + + const nfts = firstTenNfts.filter((x) => !x.compression.compressed) + const nftRemainingAccounts: AccountData[] = [] + const clientProgramId = program.programId + for (const nft of nfts) { + const { nftActionTicket } = getNftActionTicketProgramAddress( + ticketType, + registrar, + walletPk, + nft.id, + clientProgramId + ) + + const tokenAccount = await getAssociatedTokenAddress( + new PublicKey(nft.id), + walletPk, + true + ) + const metadata = await fetchNFTbyMint( + program.provider.connection, + new PublicKey(nft.id) + ) + nftRemainingAccounts.push( + new AccountData(tokenAccount), + new AccountData(metadata?.result?.metadataAddress || ''), + new AccountData(nftActionTicket, false, true) + ) + + nftActionTicketAccounts.push(new AccountData(nftActionTicket, false, true)) + } + + const nftChunks = chunks(nftRemainingAccounts, 15) + for (const chunk of [...nftChunks]) { + createNftTicketIxs.push( + await program.methods + .createNftActionTicket({ [type]: {} }) + .accounts({ + registrar, + voterWeightRecord: voterWeightPk, + voterAuthority: walletPk, + payer: walletPk, + systemProgram: SYSTEM_PROGRAM_ID, + }) + .remainingAccounts(chunk) + .instruction() + ) + } + + const compressedNfts = firstTenNfts.filter((x) => x.compression.compressed) + for (const cnft of compressedNfts) { + const { nftActionTicket } = getNftActionTicketProgramAddress( + ticketType, + registrar, + walletPk, + cnft.id, + clientProgramId + ) + + const { param, additionalAccounts } = await getCnftParamAndProof( + program.provider.connection, + cnft + ) + const instruction = await program.methods + .createCnftActionTicket({ [type]: {} }, [param]) + .accounts({ + registrar, + voterWeightRecord: voterWeightPk, + payer: walletPk, + compressionProgram: ACCOUNT_COMPACTION_PROGRAM_ID, + systemProgram: SYSTEM_PROGRAM_ID, + }) + .remainingAccounts([ + ...additionalAccounts.map((x) => new AccountData(x)), + new AccountData(nftActionTicket, false, true), + ]) + .instruction() + createNftTicketIxs.push(instruction) + + nftActionTicketAccounts.push(new AccountData(nftActionTicket, false, true)) + } + + const updateVoterWeightRecordIx = await program.methods + .updateVoterWeightRecord({ [type]: {} }) + .accounts({ + registrar: registrar, + voterWeightRecord: voterWeightPk, + payer: walletPk, + }) + .remainingAccounts(nftActionTicketAccounts) + .instruction() + + return { createNftTicketIxs, updateVoterWeightRecordIx } +} diff --git a/utils/uiTypes/NftVoterClient.ts b/utils/uiTypes/NftVoterClient.ts index 1f765ff547..c9b5369e88 100644 --- a/utils/uiTypes/NftVoterClient.ts +++ b/utils/uiTypes/NftVoterClient.ts @@ -1,15 +1,26 @@ import { Program, Provider } from '@project-serum/anchor' import { PublicKey } from '@solana/web3.js' import { NftVoter, IDL } from '../../idls/nft_voter' -import { DEFAULT_NFT_VOTER_PLUGIN } from '@tools/constants' +import { NftVoterV2, IDLV2 } from '../../idls/nft_voter_v2' +import { + DEFAULT_NFT_VOTER_PLUGIN, + DEFAULT_NFT_VOTER_PLUGIN_V2, +} from '@tools/constants' +import { ON_NFT_VOTER_V2 } from '@constants/flags' -export class NftVoterClient { +// const programVersion = (ON_NFT_VOTER_V2 ? Program : Program) +// const idl = ON_NFT_VOTER_V2 ? IDLV2 : IDL +const DEFAULT_NFT_VOTER_PLUGIN_VERSION = ON_NFT_VOTER_V2 + ? DEFAULT_NFT_VOTER_PLUGIN_V2 + : DEFAULT_NFT_VOTER_PLUGIN + +export class NftVoterClientV1 { constructor(public program: Program, public devnet?: boolean) {} static connect( provider: Provider, devnet?: boolean, - programId = new PublicKey(DEFAULT_NFT_VOTER_PLUGIN) + programId = new PublicKey(DEFAULT_NFT_VOTER_PLUGIN_VERSION) ): NftVoterClient { return new NftVoterClient( new Program(IDL, programId, provider), @@ -17,3 +28,46 @@ export class NftVoterClient { ) } } + +export class NftVoterClientV2 { + constructor(public program: Program, public devnet?: boolean) {} + + static connect( + provider: Provider, + devnet?: boolean, + programId = new PublicKey(DEFAULT_NFT_VOTER_PLUGIN_VERSION) + ): NftVoterClient { + console.log(programId.toBase58()) + return new NftVoterClient( + new Program(IDLV2, programId, provider), + devnet + ) + } +} + +export class NftVoterClient { + constructor( + public program: Program | Program, + public devnet?: boolean + ) {} + + static connect( + provider: Provider, + devnet?: boolean, + programId = new PublicKey(DEFAULT_NFT_VOTER_PLUGIN_VERSION) + ): NftVoterClient { + if (ON_NFT_VOTER_V2) { + return NftVoterClientV2.connect( + provider, + devnet, + programId + ) as NftVoterClient + } else { + return NftVoterClientV1.connect( + provider, + devnet, + programId + ) as NftVoterClient + } + } +} diff --git a/utils/uiTypes/VotePlugin.ts b/utils/uiTypes/VotePlugin.ts index 00f5b371f6..a6c07695c3 100644 --- a/utils/uiTypes/VotePlugin.ts +++ b/utils/uiTypes/VotePlugin.ts @@ -15,6 +15,7 @@ import { getVoterWeightPDA, } from 'VoteStakeRegistry/sdk/accounts' import { NFTWithMint } from './nfts' +import { DasNftObject } from '@hooks/queries/digitalAssets' import { getPreviousVotingWeightRecord, getVoteInstruction, @@ -25,10 +26,7 @@ import { getMaxVoterWeightRecord as getPluginMaxVoterWeightRecord, } from '@utils/plugin/accounts' import { VsrClient } from 'VoteStakeRegistry/sdk/client' -import { - getNftVoteRecordProgramAddress, - getUsedNftsForProposal, -} from 'NftVotePlugin/accounts' +import { getUsedNftsForProposal } from 'NftVotePlugin/accounts' import { PositionWithMeta } from 'HeliumVotePlugin/sdk/types' import { HeliumVsrClient } from 'HeliumVotePlugin/sdk/client' import { @@ -43,8 +41,20 @@ import { getAssociatedTokenAddress } from '@blockworks-foundation/mango-v4' import { NftVoterClient } from './NftVoterClient' import queryClient from '@hooks/queries/queryClient' import asFindable from '@utils/queries/asFindable' - -type UpdateVoterWeightRecordTypes = +import { ON_NFT_VOTER_V2 } from '@constants/flags' +import { + getUpdateVoterWeightRecordInstruction, + getUpdateVoterWeightRecordInstructionV2, +} from '@utils/instructions/NftVoter/updateVoterWeight' +import { + getCastNftVoteInstruction, + getCastNftVoteInstructionV2, +} from '@utils/instructions/NftVoter/castNftVote' +import { NftVoter } from 'idls/nft_voter' +import { NftVoterV2 } from 'idls/nft_voter_v2' +import { Program } from '@project-serum/anchor' + +export type UpdateVoterWeightRecordTypes = | 'castVote' | 'commentProposal' | 'createGovernance' @@ -69,7 +79,7 @@ export enum VotingClientType { GatewayClient, } -class AccountData { +export class AccountData { pubkey: PublicKey isSigner: boolean isWritable: boolean @@ -100,7 +110,7 @@ export class VotingClient { client: Client | undefined realm: ProgramAccount | undefined walletPk: PublicKey | null | undefined - votingNfts: NFTWithMeta[] + votingNfts: DasNftObject[] heliumVsrVotingPositions: PositionWithMeta[] gatewayToken: PublicKey oracles: PublicKey[] @@ -142,10 +152,11 @@ export class VotingClient { withUpdateVoterWeightRecord = async ( instructions: TransactionInstruction[], tokenOwnerRecord: ProgramAccount, - type: UpdateVoterWeightRecordTypes + type: UpdateVoterWeightRecordTypes, + createNftActionTicketIxs?: TransactionInstruction[] ): Promise => { const realm = this.realm! - + console.log(this.client) if ( this.noClient || !realm.account.communityMint.equals( @@ -248,25 +259,35 @@ export class VotingClient { clientProgramId, instructions ) - const remainingAccounts: AccountData[] = [] - for (let i = 0; i < this.votingNfts.length; i++) { - const nft = this.votingNfts[i] - const tokenAccount = await nft.getAssociatedTokenAccount() - remainingAccounts.push( - new AccountData(tokenAccount), - new AccountData(nft.address) + if (!ON_NFT_VOTER_V2) { + console.log('on nft voter v1') + const updateVoterWeightRecordIx = await getUpdateVoterWeightRecordInstruction( + this.client.program as Program, + walletPk, + registrar, + voterWeightPk, + this.votingNfts, + type + ) + instructions.push(updateVoterWeightRecordIx) + } else { + console.log('on nft voter v2') + const { + createNftTicketIxs, + updateVoterWeightRecordIx, + } = await getUpdateVoterWeightRecordInstructionV2( + this.client.program as Program, + walletPk, + registrar, + voterWeightPk, + this.votingNfts, + type ) + createNftActionTicketIxs?.push(...createNftTicketIxs) + instructions.push(updateVoterWeightRecordIx) } - const updateVoterWeightRecordIx = await this.client.program.methods - .updateVoterWeightRecord({ [type]: {} }) - .accounts({ - registrar: registrar, - voterWeightRecord: voterWeightPk, - }) - .remainingAccounts(remainingAccounts.slice(0, 10)) - .instruction() - instructions.push(updateVoterWeightRecordIx) + return { voterWeightPk, maxVoterWeightRecord } } if (this.client instanceof GatewayClient) { @@ -293,7 +314,8 @@ export class VotingClient { withCastPluginVote = async ( instructions: TransactionInstruction[], proposal: ProgramAccount, - tokenOwnerRecord: ProgramAccount + tokenOwnerRecord: ProgramAccount, + createNftActionTicketIxs?: TransactionInstruction[] ): Promise => { if (this.noClient) { return @@ -407,7 +429,6 @@ export class VotingClient { } if (this.client instanceof NftVoterClient) { - const remainingAccounts: AccountData[] = [] const { registrar } = await getPluginRegistrarPDA( realm.pubkey, realm.account.communityMint, @@ -428,44 +449,34 @@ export class VotingClient { this.client, proposal.pubkey ) - for (let i = 0; i < this.votingNfts.length; i++) { - const nft = this.votingNfts[i] - const tokenAccount = await nft.getAssociatedTokenAccount() - const { nftVoteRecord } = await getNftVoteRecordProgramAddress( + if (!ON_NFT_VOTER_V2) { + const castNftVoteIxs = await getCastNftVoteInstruction( + this.client.program as Program, + walletPk, + registrar, proposal.pubkey, - nft.mintAddress, - clientProgramId - ) - if ( - !nftVoteRecordsFiltered.find( - (x) => x.publicKey.toBase58() === nftVoteRecord.toBase58() - ) + tokenOwnerRecord.pubkey, + voterWeightPk, + this.votingNfts, + nftVoteRecordsFiltered ) - remainingAccounts.push( - new AccountData(tokenAccount), - new AccountData(nft.address), - new AccountData(nftVoteRecord, false, true) - ) - } - - //1 nft is 3 accounts - const nftChunks = chunks(remainingAccounts, 12) - - for (const chunk of [...nftChunks]) { - instructions.push( - await this.client.program.methods - .castNftVote(proposal.pubkey) - .accounts({ - registrar, - voterWeightRecord: voterWeightPk, - voterTokenOwnerRecord: tokenOwnerRecord.pubkey, - voterAuthority: walletPk, - payer: walletPk, - systemProgram: SYSTEM_PROGRAM_ID, - }) - .remainingAccounts(chunk) - .instruction() + instructions.push(...castNftVoteIxs) + } else { + const { + castNftVoteTicketIxs, + castNftVoteIxs, + } = await getCastNftVoteInstructionV2( + this.client.program as Program, + walletPk, + registrar, + proposal.pubkey, + tokenOwnerRecord.pubkey, + voterWeightPk, + this.votingNfts, + nftVoteRecordsFiltered ) + createNftActionTicketIxs?.push(...castNftVoteTicketIxs) + instructions.push(...castNftVoteIxs) } return { voterWeightPk, maxVoterWeightRecord } @@ -690,7 +701,7 @@ export class VotingClient { voterWeightRecordBump, } } - _setCurrentVoterNfts = (nfts: NFTWithMeta[]) => { + _setCurrentVoterNfts = (nfts: DasNftObject[]) => { this.votingNfts = nfts } _setCurrentHeliumVsrPositions = (positions: PositionWithMeta[]) => {