From 8e466233d67ef4569495123f2dfdeebc1a84c723 Mon Sep 17 00:00:00 2001 From: Bryan Date: Mon, 15 May 2023 18:02:56 -0500 Subject: [PATCH] small tweaks (#1610) --- .../components/ClaimUnreleasedPositions.tsx | 178 ++++++++++++++++++ .../components/LockTokensAccount.tsx | 49 ++--- .../components/VotingPowerCard.tsx | 30 +-- HeliumVotePlugin/hooks/useTransferPosition.ts | 4 +- HeliumVotePlugin/sdk/types.ts | 2 +- HeliumVotePlugin/utils/getPositions.ts | 4 +- actions/castVote.ts | 8 +- .../LockedCommunityNFTRecordVotingPower.tsx | 8 +- .../TokenBalance/TokenBalanceCardWrapper.tsx | 9 +- components/VoteCommentModal.tsx | 125 ++---------- components/VotePanel/CastVoteButtons.tsx | 33 ++-- components/VotePanel/VetoButtons.tsx | 21 ++- components/chat/DiscussionPanel.tsx | 34 ++-- hooks/useRealm.tsx | 4 + hooks/useSubmitVote.ts | 103 ++++++++++ models/registry/api.ts | 1 + package.json | 2 +- pages/dao/[symbol]/proposal/[pk]/index.tsx | 6 +- public/realms/devnet.json | 5 +- public/realms/mainnet-beta.json | 19 +- utils/uiTypes/VotePlugin.ts | 22 +-- yarn.lock | 8 +- 22 files changed, 443 insertions(+), 232 deletions(-) create mode 100644 HeliumVotePlugin/components/ClaimUnreleasedPositions.tsx create mode 100644 hooks/useSubmitVote.ts diff --git a/HeliumVotePlugin/components/ClaimUnreleasedPositions.tsx b/HeliumVotePlugin/components/ClaimUnreleasedPositions.tsx new file mode 100644 index 0000000000..30ad4b7113 --- /dev/null +++ b/HeliumVotePlugin/components/ClaimUnreleasedPositions.tsx @@ -0,0 +1,178 @@ +import React, { useEffect, useState } from 'react' +import useRealm from '@hooks/useRealm' +import useWalletStore from 'stores/useWalletStore' +import { TransactionInstruction } from '@solana/web3.js' +import { SecondaryButton } from '@components/Button' +import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' +import { HeliumVsrClient } from 'HeliumVotePlugin/sdk/client' +import { chunks } from '@utils/helpers' +import { + registrarKey, + positionKey, + voterWeightRecordKey, +} from '@helium/voter-stake-registry-sdk' +import { + sendTransactionsV3, + SequenceType, + txBatchesToInstructionSetWithSigners, +} from '@utils/sendTransactions' +import { ProposalState } from '@solana/spl-governance' +import useWalletOnePointOh from '@hooks/useWalletOnePointOh' +import { useAddressQuery_CommunityTokenOwner } from '@hooks/queries/addresses/tokenOwner' + +const NFT_SOL_BALANCE = 0.0014616 + +const ClaimUnreleasedPositions = ({ + inAccountDetails, +}: { + inAccountDetails?: boolean +}) => { + const wallet = useWalletOnePointOh() + const [isLoading, setIsLoading] = useState(false) + const { current: connection } = useWalletStore((s) => s.connection) + const [ownVoteRecords, setOwnVoteRecords] = useState([]) + const [solToBeClaimed, setSolToBeClaimed] = useState(0) + const { realm } = useWalletStore((s) => s.selectedRealm) + const votingPlugin = useVotePluginsClientStore( + (s) => s.state.currentRealmVotingClient + ) + + const { proposals } = useRealm() + const { data: tokenOwnerRecord } = useAddressQuery_CommunityTokenOwner() + const isHeliumVsr = votingPlugin.client instanceof HeliumVsrClient + + const releasePositions = async () => { + if (!wallet?.publicKey) throw new Error('no wallet') + if (!realm) throw new Error() + if (!tokenOwnerRecord) throw new Error() + + setIsLoading(true) + const instructions: TransactionInstruction[] = [] + const [registrar] = registrarKey( + realm.pubkey, + realm.account.communityMint, + votingPlugin.client!.program.programId + ) + + const [voterWeightPk] = voterWeightRecordKey( + registrar, + wallet.publicKey, + votingPlugin.client!.program.programId + ) + + const voteRecords = ownVoteRecords + for (const i of voteRecords) { + const proposal = proposals[i.account.proposal.toBase58()] + const [posKey] = positionKey( + i.account.nftMint, + votingPlugin.client!.program.programId + ) + if (proposal.account.state === ProposalState.Voting) { + // ignore this one as it's still in voting + continue + } + + const relinquishVoteIx = await (votingPlugin.client as HeliumVsrClient).program.methods + .relinquishVoteV0() + .accounts({ + registrar, + voterWeightRecord: voterWeightPk, + governance: proposal.account.governance, + proposal: i.account.proposal, + voterTokenOwnerRecord: tokenOwnerRecord, + voterAuthority: wallet.publicKey, + voteRecord: i.publicKey, + beneficiary: wallet!.publicKey!, + }) + .remainingAccounts([ + { pubkey: i.publicKey, isSigner: false, isWritable: true }, + { pubkey: posKey, isSigner: false, isWritable: true }, + ]) + .instruction() + instructions.push(relinquishVoteIx) + } + try { + const insertChunks = chunks(instructions, 10).map((txBatch, batchIdx) => { + return { + instructionsSet: txBatchesToInstructionSetWithSigners( + txBatch, + [], + batchIdx + ), + sequenceType: SequenceType.Parallel, + } + }) + await sendTransactionsV3({ + connection, + wallet: wallet!, + transactionInstructions: insertChunks, + }) + setIsLoading(false) + getVoteRecords() + } catch (e) { + setIsLoading(false) + console.log(e) + } + } + const getVoteRecords = async () => { + const currentClient = votingPlugin.client as HeliumVsrClient + const voteRecords = + (await currentClient.program.account['nftVoteRecord']?.all([ + { + memcmp: { + offset: 72, + bytes: wallet!.publicKey!.toBase58(), + }, + }, + ])) || [] + + const voteRecordsFiltered = voteRecords.filter( + (x) => + proposals[x.account.proposal.toBase58()] && + proposals[ + x.account.proposal.toBase58() + ].account.governingTokenMint.toBase58() === + realm?.account.communityMint.toBase58() && + proposals[x.account.proposal.toBase58()].account.state !== + ProposalState.Voting + ) + setOwnVoteRecords(voteRecordsFiltered) + setSolToBeClaimed(voteRecordsFiltered.length * NFT_SOL_BALANCE) + } + + useEffect(() => { + if (wallet?.publicKey && isHeliumVsr && votingPlugin.client) { + getVoteRecords() + } + // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO please fix, it can cause difficult bugs. You might wanna check out https://bobbyhadz.com/blog/react-hooks-exhaustive-deps for info. -@asktree + }, [votingPlugin.clientType, isHeliumVsr, wallet?.publicKey?.toBase58()]) + + if (isHeliumVsr) { + return ( + <> + {((!inAccountDetails && solToBeClaimed > 0) || + (inAccountDetails && solToBeClaimed != 0)) && ( +
+
+
+ Relinquish your old votes and claim {solToBeClaimed.toFixed(4)}{' '} + SOL from past proposal voting costs. +
+ releasePositions()} + className="w-1/2 max-w-[200px]" + > + Relinquish + +
+
+ )} + + ) + } else { + return null + } +} + +export default ClaimUnreleasedPositions diff --git a/HeliumVotePlugin/components/LockTokensAccount.tsx b/HeliumVotePlugin/components/LockTokensAccount.tsx index a694c97c28..c12be7225a 100644 --- a/HeliumVotePlugin/components/LockTokensAccount.tsx +++ b/HeliumVotePlugin/components/LockTokensAccount.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useState } from 'react' import { useAsync } from 'react-async-hook' import { BN } from '@coral-xyz/anchor' import { @@ -96,6 +96,21 @@ export const LockTokensAccount: React.FC<{ s.getPositions, ]) + const sortedPositions = useMemo( + () => + positions.sort((a, b) => { + if (a.hasGenesisMultiplier || b.hasGenesisMultiplier) { + if (b.hasGenesisMultiplier) { + return a.amountDepositedNative.gt(b.amountDepositedNative) ? 0 : -1 + } + return -1 + } + + return a.amountDepositedNative.gt(b.amountDepositedNative) ? -1 : 0 + }), + [positions] + ) + useEffect(() => { if (subDaosError) { notify({ @@ -320,26 +335,18 @@ export const LockTokensAccount: React.FC<{ }`} > {!loading && - positions - .sort((a, b) => - a.hasGenesisMultiplier - ? b.hasGenesisMultiplier - ? 0 - : -1 - : 1 - ) - .map((pos, idx) => ( - - ))} + sortedPositions.map((pos, idx) => ( + + ))} {isSameWallet && (
diff --git a/HeliumVotePlugin/components/VotingPowerCard.tsx b/HeliumVotePlugin/components/VotingPowerCard.tsx index 560720dff8..de245eab4f 100644 --- a/HeliumVotePlugin/components/VotingPowerCard.tsx +++ b/HeliumVotePlugin/components/VotingPowerCard.tsx @@ -1,34 +1,29 @@ import React, { useEffect, useState } from 'react' -import Link from 'next/link' import { BN } from '@coral-xyz/anchor' import { GoverningTokenRole } from '@solana/spl-governance' import useRealm from '@hooks/useRealm' import { fmtMintAmount } from '@tools/sdk/units' import { getMintMetadata } from '@components/instructions/programs/splToken' -import useQueryContext from '@hooks/useQueryContext' -import { ChevronRightIcon } from '@heroicons/react/solid' import InlineNotification from '@components/InlineNotification' import DelegateTokenBalanceCard from '@components/TokenBalance/DelegateTokenBalanceCard' import { TokenDeposit } from '@components/TokenBalance/TokenBalanceCard' import useHeliumVsrStore from 'HeliumVotePlugin/hooks/useHeliumVsrStore' import { MintInfo } from '@solana/spl-token' import { VotingPowerBox } from './VotingPowerBox' -import { useAddressQuery_CommunityTokenOwner } from '@hooks/queries/addresses/tokenOwner' import useWalletOnePointOh from '@hooks/useWalletOnePointOh' export const VotingPowerCard: React.FC<{ inAccountDetails?: boolean }> = ({ inAccountDetails }) => { - const { fmtUrlWithCluster } = useQueryContext() + const loading = useHeliumVsrStore((s) => s.state.isLoading) const [hasGovPower, setHasGovPower] = useState(false) - const { councilMint, ownTokenRecord, mint, symbol } = useRealm() + const { councilMint, ownTokenRecord, mint } = useRealm() const wallet = useWalletOnePointOh() const connected = !!wallet?.connected const councilDepositVisible = !!councilMint - const { data: tokenOwnerRecordPk } = useAddressQuery_CommunityTokenOwner() - const isLoading = !mint || !councilMint + const isLoading = !mint || !councilMint || loading const isSameWallet = (connected && !ownTokenRecord) || (connected && @@ -37,25 +32,6 @@ export const VotingPowerCard: React.FC<{ return ( <> -
-

My governance power

- - - View - - - -
{!isLoading ? ( <> {!hasGovPower && !inAccountDetails && connected && ( diff --git a/HeliumVotePlugin/hooks/useTransferPosition.ts b/HeliumVotePlugin/hooks/useTransferPosition.ts index b1ad393fcc..49dad2fc9c 100644 --- a/HeliumVotePlugin/hooks/useTransferPosition.ts +++ b/HeliumVotePlugin/hooks/useTransferPosition.ts @@ -42,7 +42,9 @@ export const useTransferPosition = () => { !mint || !wallet || !client || - !(client instanceof HeliumVsrClient) + !(client instanceof HeliumVsrClient) || + sourcePosition.numActiveVotes > 0 || + targetPosition.numActiveVotes > 0 const idl = await Program.fetchIdl(programId, provider) const hsdProgram = await init(provider as any, programId, idl) diff --git a/HeliumVotePlugin/sdk/types.ts b/HeliumVotePlugin/sdk/types.ts index a839580144..bc4d2c2590 100644 --- a/HeliumVotePlugin/sdk/types.ts +++ b/HeliumVotePlugin/sdk/types.ts @@ -10,7 +10,7 @@ export type VotingMintConfig = IdlTypes['VotingMintCon type RegistrarV0 = IdlAccounts['registrar'] export type Lockup = IdlTypes['Lockup'] export type PositionV0 = IdlAccounts['positionV0'] -export type DelegatedPostionV0 = IdlAccounts['delegatedPositionV0'] +export type DelegatedPositionV0 = IdlAccounts['delegatedPositionV0'] export interface Registrar extends RegistrarV0 { votingMints: VotingMintConfig[] } diff --git a/HeliumVotePlugin/utils/getPositions.ts b/HeliumVotePlugin/utils/getPositions.ts index cc5b46e455..e8a8c87c79 100644 --- a/HeliumVotePlugin/utils/getPositions.ts +++ b/HeliumVotePlugin/utils/getPositions.ts @@ -15,7 +15,7 @@ import { Registrar, PositionWithMeta, PositionV0, - DelegatedPostionV0, + DelegatedPositionV0, } from '../sdk/types' import { chunks } from '@utils/helpers' import { HNT_MINT } from '@helium/spl-utils' @@ -94,7 +94,7 @@ export const getPositions = async ( ? (hsdProgram.coder.accounts.decode( 'DelegatedPositionV0', delegatedPos.data - ) as DelegatedPostionV0) + ) as DelegatedPositionV0) : null ) })() diff --git a/actions/castVote.ts b/actions/castVote.ts index 19ec9db0bd..23e6ab6168 100644 --- a/actions/castVote.ts +++ b/actions/castVote.ts @@ -200,9 +200,11 @@ export async function castVote( wallet, transactionInstructions: ixsChunks, callbacks: { - afterFirstBatchSign: () => (ixsChunks.length > 2 ? null : null), - afterAllTxConfirmed: () => - runAfterConfirmation ? runAfterConfirmation() : null, + afterAllTxConfirmed: () => { + if (runAfterConfirmation) { + runAfterConfirmation() + } + }, }, }) } diff --git a/components/ProposalVotingPower/LockedCommunityNFTRecordVotingPower.tsx b/components/ProposalVotingPower/LockedCommunityNFTRecordVotingPower.tsx index e4e86220a6..95a4275cc0 100644 --- a/components/ProposalVotingPower/LockedCommunityNFTRecordVotingPower.tsx +++ b/components/ProposalVotingPower/LockedCommunityNFTRecordVotingPower.tsx @@ -1,5 +1,4 @@ import React, { useState, useEffect } from 'react' -import classNames from 'classnames' import { BigNumber } from 'bignumber.js' import useRealm from '@hooks/useRealm' import useHeliumVsrStore from 'HeliumVotePlugin/hooks/useHeliumVsrStore' @@ -88,9 +87,10 @@ export default function LockedCommunityNFTRecordVotingPower(props: Props) { if (isLoading) { return ( -
+ <> +
+
+ ) } diff --git a/components/TokenBalance/TokenBalanceCardWrapper.tsx b/components/TokenBalance/TokenBalanceCardWrapper.tsx index 92ae1ddb41..b92af5dc03 100644 --- a/components/TokenBalance/TokenBalanceCardWrapper.tsx +++ b/components/TokenBalance/TokenBalanceCardWrapper.tsx @@ -14,6 +14,7 @@ import ClaimUnreleasedNFTs from './ClaimUnreleasedNFTs' import Link from 'next/link' import { useAddressQuery_CommunityTokenOwner } from '@hooks/queries/addresses/tokenOwner' import useWalletOnePointOh from '@hooks/useWalletOnePointOh' +import ClaimUnreleasedPositions from 'HeliumVotePlugin/components/ClaimUnreleasedPositions' const LockPluginTokenBalanceCard = dynamic( () => @@ -103,7 +104,13 @@ const TokenBalanceCardWrapper = ({ (!ownTokenRecord || ownTokenRecord.account.governingTokenDepositAmount.isZero()) ) { - return + return ( + <> + {!inAccountDetails && } + + + + ) } if ( diff --git a/components/VoteCommentModal.tsx b/components/VoteCommentModal.tsx index b31019dc55..077fb25ec8 100644 --- a/components/VoteCommentModal.tsx +++ b/components/VoteCommentModal.tsx @@ -1,31 +1,16 @@ import React, { FunctionComponent, useState } from 'react' import { BanIcon, ThumbDownIcon, ThumbUpIcon } from '@heroicons/react/solid' -import { - ChatMessageBody, - ChatMessageBodyType, - VoteKind, -} from '@solana/spl-governance' -import { RpcContext } from '@solana/spl-governance' +import { VoteKind } from '@solana/spl-governance' import useWalletStore from '../stores/useWalletStore' -import useRealm from '../hooks/useRealm' -import { castVote } from '../actions/castVote' import Button, { SecondaryButton } from './Button' -// import { notify } from '../utils/notifications' import Loading from './Loading' import Modal from './Modal' import Input from './inputs/Input' import Tooltip from './Tooltip' import { TokenOwnerRecord } from '@solana/spl-governance' import { ProgramAccount } from '@solana/spl-governance' -import { getProgramVersionForRealm } from '@models/registry/api' -import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' -import { nftPluginsPks } from '@hooks/useVotingPlugins' -import useNftProposalStore from 'NftVotePlugin/NftProposalStore' -import { NftVoterClient } from '@utils/uiTypes/NftVoterClient' -import queryClient from '@hooks/queries/queryClient' -import { voteRecordQueryKeys } from '@hooks/queries/voteRecord' -import useWalletOnePointOh from '@hooks/useWalletOnePointOh' +import { useSubmitVote } from '@hooks/useSubmitVote' interface VoteCommentModalProps { onClose: () => void @@ -34,92 +19,6 @@ interface VoteCommentModalProps { voterTokenRecord: ProgramAccount } -const useSubmitVote = ({ - comment, - onClose, - voterTokenRecord, -}: { - comment: string - onClose: () => void - voterTokenRecord: ProgramAccount -}) => { - const client = useVotePluginsClientStore( - (s) => s.state.currentRealmVotingClient - ) - const [submitting, setSubmitting] = useState(false) - const wallet = useWalletOnePointOh() - const connection = useWalletStore((s) => s.connection) - const { proposal } = useWalletStore((s) => s.selectedProposal) - const { fetchChatMessages } = useWalletStore((s) => s.actions) - const { realm, realmInfo, config } = useRealm() - const { refetchProposals } = useWalletStore((s) => s.actions) - const isNftPlugin = - config?.account.communityTokenConfig.voterWeightAddin && - nftPluginsPks.includes( - config?.account.communityTokenConfig.voterWeightAddin?.toBase58() - ) - const { closeNftVotingCountingModal } = useNftProposalStore.getState() - const submitVote = async (vote: VoteKind) => { - setSubmitting(true) - const rpcContext = new RpcContext( - proposal!.owner, - getProgramVersionForRealm(realmInfo!), - wallet!, - connection.current, - connection.endpoint - ) - - const msg = comment - ? new ChatMessageBody({ - type: ChatMessageBodyType.Text, - value: comment, - }) - : undefined - - const confirmationCallback = async () => { - await refetchProposals() - // TODO refine this to only invalidate the one query - await queryClient.invalidateQueries( - voteRecordQueryKeys.all(connection.cluster) - ) - } - - try { - await castVote( - rpcContext, - realm!, - proposal!, - voterTokenRecord, - vote, - msg, - client, - confirmationCallback - ) - if (!isNftPlugin) { - await refetchProposals() - } - } catch (ex) { - if (isNftPlugin) { - closeNftVotingCountingModal( - (client.client as unknown) as NftVoterClient, - proposal!, - wallet!.publicKey! - ) - } - //TODO: How do we present transaction errors to users? Just the notification? - console.error("Can't cast vote", ex) - onClose() - } finally { - setSubmitting(false) - onClose() - } - - fetchChatMessages(proposal!.pubkey) - } - - return { submitting, submitVote } -} - const VOTE_STRINGS = { [VoteKind.Approve]: 'Yes', [VoteKind.Deny]: 'No', @@ -133,15 +32,23 @@ const VoteCommentModal: FunctionComponent = ({ vote, voterTokenRecord, }) => { + const { fetchChatMessages } = useWalletStore((s) => s.actions) + const { proposal } = useWalletStore((s) => s.selectedProposal) const [comment, setComment] = useState('') - const { submitting, submitVote } = useSubmitVote({ - comment, - onClose, - voterTokenRecord, - }) + const { submitting, submitVote } = useSubmitVote() const voteString = VOTE_STRINGS[vote] + const handleSubmit = async () => { + await submitVote({ + vote, + voterTokenRecord, + comment, + }) + onClose() + await fetchChatMessages(proposal!.pubkey) + } + return (

Confirm your vote

@@ -168,7 +75,7 @@ const VoteCommentModal: FunctionComponent = ({