diff --git a/hooks/useGovernanceAssets.ts b/hooks/useGovernanceAssets.ts index f8b820b8d..1bb744ec1 100644 --- a/hooks/useGovernanceAssets.ts +++ b/hooks/useGovernanceAssets.ts @@ -380,6 +380,11 @@ export default function useGovernanceAssets() { isVisible: canUseTransferInstruction, packageId: PackageEnum.Common, }, + [Instructions.RelinquishDaoVote]: { + name: 'Relinquish Vote from DAO', + isVisible: canUseTransferInstruction, + packageId: PackageEnum.Common, + }, [Instructions.DualFinanceVoteDeposit]: { name: 'Join a VSR DAO', isVisible: canUseTransferInstruction, diff --git a/pages/dao/[symbol]/proposal/components/instructions/RelinquishDaoVote.tsx b/pages/dao/[symbol]/proposal/components/instructions/RelinquishDaoVote.tsx new file mode 100644 index 000000000..9bf00044e --- /dev/null +++ b/pages/dao/[symbol]/proposal/components/instructions/RelinquishDaoVote.tsx @@ -0,0 +1,216 @@ +import Input from '@components/inputs/Input' +import useGovernanceAssets from '@hooks/useGovernanceAssets' +import { + getGovernanceAccounts, + getProposal, + getTokenOwnerRecordAddress, + getVoteRecordAddress, + Governance, + ProgramAccount, + pubkeyFilter, + serializeInstructionToBase64, + TOKEN_PROGRAM_ID, + withRelinquishVote, +} from '@solana/spl-governance' +import { PublicKey, TransactionInstruction } from '@solana/web3.js' +import { + RelinquishDaoVoteForm, + UiInstruction, +} from '@utils/uiTypes/proposalCreationTypes' +import { useRouter } from 'next/router' +import { useContext, useEffect, useState } from 'react' +import GovernedAccountSelect from '../GovernedAccountSelect' +import { NewProposalContext } from '../../new' +import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext' +import useWalletOnePointOh from '@hooks/useWalletOnePointOh' +import { fetchProgramVersion } from '@hooks/queries/useProgramVersionQuery' +import { getRealm } from '@realms-today/spl-governance' +import { + createAssociatedTokenAccountIdempotentInstruction, + getAssociatedTokenAddressSync, +} from '@solana/spl-token-new' + +const RelinquishDaoVote = ({ + index, + governance, +}: { + index: number + governance: ProgramAccount | null +}) => { + const wallet = useWalletOnePointOh() + const connection = useLegacyConnectionContext() + + const { governedTokenAccounts } = useGovernanceAssets() + + const { handleSetInstructions } = useContext(NewProposalContext) + + const [form, setForm] = useState({ + governedAccount: undefined, + mintInfo: undefined, + realm: '', + proposal: '', + }) + + const [formErrors, setFormErrors] = useState({}) + const handleSetForm = ({ propertyName, value }) => { + setFormErrors({}) + setForm({ ...form, [propertyName]: value }) + } + + + const setRealm = (event) => { + const value = event.target.value + handleSetForm({ + value: value, + propertyName: 'realm', + }) + } + + const setProposal = (event) => { + const value = event.target.value + handleSetForm({ + value: value, + propertyName: 'proposal', + }) + } + + async function getInstruction() { + if ( + !connection || + !form.realm || + !form.governedAccount?.governance.account || + !form.proposal || + !wallet?.publicKey + ) { + return { + serializedInstruction: '', + isValid: false, + governance: form.governedAccount?.governance, + } + } + const realm = await getRealm(connection.current, new PublicKey(form.realm)) + + const instructions: TransactionInstruction[] = [] + const prerequisiteInstructions: TransactionInstruction[] = [] + + const programVersion = await fetchProgramVersion( + connection.current, + realm.owner, + ) + + const mint = realm.account.communityMint + const destinationAccount = getAssociatedTokenAddressSync( + mint, + form.governedAccount.extensions.transferAddress!, + true, + TOKEN_PROGRAM_ID, + ) + + const createaAta = createAssociatedTokenAccountIdempotentInstruction( + wallet.publicKey, + destinationAccount, + form.governedAccount.extensions.transferAddress!, + mint, + ) + prerequisiteInstructions.push(createaAta) + + const voterTokenRecord = await getTokenOwnerRecordAddress( + realm.owner, + realm.pubkey, + realm.account.communityMint, + form.governedAccount.extensions.transferAddress!, + ) + + const proposal = await getProposal(connection.current, new PublicKey(form.proposal)) + + const voteRecordAddress = await getVoteRecordAddress( + realm.owner, + proposal.pubkey, + voterTokenRecord + ) + + await withRelinquishVote( + instructions, + realm!.owner, + programVersion, + realm!.pubkey, + proposal!.account.governance, + proposal!.pubkey, + voterTokenRecord, + realm.account.communityMint, + voteRecordAddress, + form.governedAccount.extensions.transferAddress, + form.governedAccount.extensions.transferAddress, + ) + + const obj: UiInstruction = { + serializedInstruction: '', + additionalSerializedInstructions: instructions.map((x) => + serializeInstructionToBase64(x), + ), + prerequisiteInstructions: prerequisiteInstructions, + isValid: true, + governance: form.governedAccount?.governance, + customHoldUpTime: 0, + chunkBy: 2, + } + return obj + } + + // Update mint info when selected token account changes. + useEffect(() => { + setForm({ + ...form, + mintInfo: form.governedAccount?.extensions.mint?.account, + }) + // 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 + }, [form.governedAccount]) + + useEffect(() => { + handleSetInstructions( + { + governedAccount: form.governedAccount?.governance, + getInstruction, + }, + index, + ) + // 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 + }, [form]) + + return ( + <> + + + {form.realm ? ( + <> + { + handleSetForm({ value, propertyName: 'governedAccount' }) + }} + value={form.governedAccount} + error={formErrors['governedAccount']} + shouldBeGoverned={!!governance} + governance={governance} + type="wallet" + /> + + ) : null} + + ) +} + +export default RelinquishDaoVote diff --git a/pages/dao/[symbol]/proposal/new.tsx b/pages/dao/[symbol]/proposal/new.tsx index c14f20ec4..e6ee3f4d5 100644 --- a/pages/dao/[symbol]/proposal/new.tsx +++ b/pages/dao/[symbol]/proposal/new.tsx @@ -157,6 +157,7 @@ import WithdrawFees from './components/instructions/Token2022/WithdrawFees' import SquadsV4RemoveMember from './components/instructions/Squads/SquadsV4RemoveMember' import CollectPoolFees from './components/instructions/Raydium/CollectPoolFees' import CollectVestedTokens from './components/instructions/Raydium/CollectVestedTokens' +import RelinquishDaoVote from './components/instructions/RelinquishDaoVote' const TITLE_LENGTH_LIMIT = 130 // the true length limit is either at the tx size level, and maybe also the total account size level (I can't remember) @@ -517,6 +518,7 @@ const New = () => { [Instructions.DualFinanceExerciseStakingOption]: DualExercise, [Instructions.DualFinanceDelegate]: DualDelegate, [Instructions.DualFinanceDelegateWithdraw]: DualVoteDepositWithdraw, + [Instructions.RelinquishDaoVote]: RelinquishDaoVote, [Instructions.DualFinanceVoteDeposit]: DualVoteDeposit, [Instructions.DaoVote]: DaoVote, [Instructions.DistributionCloseVaults]: CloseVaults, diff --git a/utils/uiTypes/proposalCreationTypes.ts b/utils/uiTypes/proposalCreationTypes.ts index 0e7bb95f3..36c4e1b5f 100644 --- a/utils/uiTypes/proposalCreationTypes.ts +++ b/utils/uiTypes/proposalCreationTypes.ts @@ -311,6 +311,13 @@ export interface WithdrawDAOForm { amount?: number } +export interface RelinquishDaoVoteForm { + governedAccount?: AssetAccount + mintInfo: MintInfo | undefined + realm: string + proposal: string +} + export enum Instructions { Base64, Burn, @@ -340,6 +347,7 @@ export enum Instructions { DualFinanceStakingOptionWithdraw, DualFinanceDelegate, DualFinanceDelegateWithdraw, + RelinquishDaoVote, DualFinanceVoteDeposit, DaoVote, DistributionCloseVaults,