diff --git a/hooks/useGovernanceAssets.ts b/hooks/useGovernanceAssets.ts index f0f320b684..c0c0c6dcf2 100644 --- a/hooks/useGovernanceAssets.ts +++ b/hooks/useGovernanceAssets.ts @@ -327,6 +327,10 @@ export default function useGovernanceAssets() { name: 'Sanctum Withdraw Stake', packageId: PackageEnum.Common, }, + [Instructions.WithdrawFromVoteAccount]: { + name: 'Withdraw from vote account', + packageId: PackageEnum.Common, + }, [Instructions.Transfer]: { name: 'Transfer Tokens', isVisible: canUseTokenTransferInstruction, diff --git a/pages/dao/[symbol]/proposal/components/instructions/Validators/WithdrawFromVoteAccount.tsx b/pages/dao/[symbol]/proposal/components/instructions/Validators/WithdrawFromVoteAccount.tsx new file mode 100644 index 0000000000..fcb7a0476b --- /dev/null +++ b/pages/dao/[symbol]/proposal/components/instructions/Validators/WithdrawFromVoteAccount.tsx @@ -0,0 +1,233 @@ +import React, { useContext, useEffect, useState } from 'react' +import Input from 'components/inputs/Input' +import * as yup from 'yup' + +import { + Governance, + ProgramAccount, + serializeInstructionToBase64, +} from '@solana/spl-governance' + +import { PublicKey, StakeProgram, TransactionInstruction, VoteProgram } from '@solana/web3.js' + +import { + UiInstruction, + ValidatorStakingForm, + ValidatorWithdrawFromVoteAccountForm, +} from '@utils/uiTypes/proposalCreationTypes' +import { NewProposalContext } from '../../../new' +import { isFormValid } from '@utils/formValidation' +import { web3 } from '@coral-xyz/anchor' +import useGovernanceAssets from '@hooks/useGovernanceAssets' +import GovernedAccountSelect from '../../GovernedAccountSelect' +import { parseMintNaturalAmountFromDecimal } from '@tools/sdk/units' +import useRealm from '@hooks/useRealm' +import useWalletOnePointOh from '@hooks/useWalletOnePointOh' +import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext' + +const WithdrawFromVoteAccount= ({ + index, + governance, +}: { + index: number + governance: ProgramAccount | null +}) => { + const connection = useLegacyConnectionContext() + const programId: PublicKey = StakeProgram.programId + const { governedTokenAccountsWithoutNfts } = useGovernanceAssets() + const shouldBeGoverned = !!(index !== 0 && governance) + const wallet = useWalletOnePointOh() + + const [form, setForm] = useState({ + validatorVoteKey: '', + authorizedWithdrawerKey: '', + toPubkey: '', + amount: 0, + governedTokenAccount: undefined, + }) + const [formErrors, setFormErrors] = useState({}) + const { handleSetInstructions } = useContext(NewProposalContext) + + const handleSetForm = ({ propertyName, value }) => { + setFormErrors({}) + setForm({ ...form, [propertyName]: value }) + } + + const [governedAccount, setGovernedAccount] = useState< + ProgramAccount | undefined + >(undefined) + + const setValidatorVoteKey = (event) => { + const value = event.target.value + handleSetForm({ + value: value, + propertyName: 'validatorVoteKey', + }) + } + + const setAuthorizedWithdrawerKey = (event) => { + const value = event.target.value + handleSetForm({ + value: value, + propertyName: 'authorizedWithdrawerKey', + }) + } + + const setToPubkey = (event) => { + const value = event.target.value + handleSetForm({ + value: value, + propertyName: 'toPubkey', + }) + } + + const setAmount = (event) => { + const value = event.target.value + handleSetForm({ + value: value, + propertyName: 'amount', + }) + } + + const validateInstruction = async (): Promise => { + const schema = yup.object().shape({ + validatorVoteKey: yup + .string() + .required('Validator vote address is required'), + authorizedWithdrawerKey: yup + .string() + .required('Authorized withdrawer key is required'), + toPubkey: yup + .string() + .required('toPubkey is required'), + amount: yup + .number() + .min(0.1, 'Amount must be positive number') + .required('Amount is required'), + }) + const { isValid, validationErrors } = await isFormValid(schema, form) + setFormErrors(validationErrors) + return isValid + } + + const { realmInfo } = useRealm() + + async function getInstruction(): Promise { + const isValid = await validateInstruction() + const governancePk = governance?.pubkey + const returnInvalid = (): UiInstruction => { + return { + serializedInstruction: '', + isValid: false, + governance: undefined, + } + } + const governanceAccount = governance?.account + + if ( + !connection || + !isValid || + !programId || + !governanceAccount || + !governancePk || + !form.governedTokenAccount?.isSol || + !wallet || + !wallet.publicKey || + !realmInfo + ) { + return returnInvalid() + } + + const prerequisiteInstructions: web3.TransactionInstruction[] = [] + + const validatorVotePK: PublicKey = new PublicKey(form.validatorVoteKey) + const withdrawAmount: number = parseMintNaturalAmountFromDecimal(form.amount!, 9) + + // There is only one ix in the tx. + // https://github.com/solana-labs/solana-web3.js/blob/79e6a873a7e4aaf326ae6f06d642394738e31265/src/programs/vote.ts#L525 + const withdrawIx: TransactionInstruction = VoteProgram.withdraw({ + authorizedWithdrawerPubkey: new PublicKey(form.authorizedWithdrawerKey), + lamports: withdrawAmount, + toPubkey: new PublicKey(form.toPubkey), + votePubkey: validatorVotePK, + }).instructions[0]; + + return { + serializedInstruction: serializeInstructionToBase64(withdrawIx), + isValid: true, + governance: form.governedTokenAccount.governance, + prerequisiteInstructions: prerequisiteInstructions, + chunkBy: 1, + } + } + + useEffect(() => { + handleSetInstructions( + { governedAccount: governedAccount, getInstruction }, + index + ) + }, [form]) + + useEffect(() => { + setGovernedAccount(form.governedTokenAccount?.governance) + }, [form.governedTokenAccount]) + + return ( + <> + x.isSol + )} + onChange={(value) => { + handleSetForm({ value, propertyName: 'governedTokenAccount' }) + }} + value={form.governedTokenAccount} + error={formErrors['governedTokenAccount']} + shouldBeGoverned={shouldBeGoverned} + governance={governance} + type="token" + > + + + + +
+ Withdraw SOL from vote account. This uses withdraw and not safeWithdraw, so check that the rent exempt amount remains. +
+ + ) +} + +export default WithdrawFromVoteAccount diff --git a/pages/dao/[symbol]/proposal/new.tsx b/pages/dao/[symbol]/proposal/new.tsx index 30fe0ba643..9573b77aec 100644 --- a/pages/dao/[symbol]/proposal/new.tsx +++ b/pages/dao/[symbol]/proposal/new.tsx @@ -61,6 +61,7 @@ import SanctumDepositStake from './components/instructions/Validators/SanctumDep import SanctumWithdrawStake from './components/instructions/Validators/SanctumWithdrawStake' import DeactivateValidatorStake from './components/instructions/Validators/DeactivateStake' import WithdrawValidatorStake from './components/instructions/Validators/WithdrawStake' +import WithdrawFromVoteAccount from './components/instructions/Validators/WithdrawFromVoteAccount' import DelegateStake from './components/instructions/Validators/DelegateStake' import SplitStake from './components/instructions/Validators/SplitStake' import useCreateProposal from '@hooks/useCreateProposal' @@ -549,6 +550,7 @@ const New = () => { [Instructions.SanctumWithdrawStake]: SanctumWithdrawStake, [Instructions.DeactivateValidatorStake]: DeactivateValidatorStake, [Instructions.WithdrawValidatorStake]: WithdrawValidatorStake, + [Instructions.WithdrawFromVoteAccount]: WithdrawFromVoteAccount, [Instructions.DelegateStake]: DelegateStake, [Instructions.RemoveStakeLock]: RemoveLockup, [Instructions.SplitStake]: SplitStake, diff --git a/utils/uiTypes/proposalCreationTypes.ts b/utils/uiTypes/proposalCreationTypes.ts index d0aaa9eaa9..e0857d56ff 100644 --- a/utils/uiTypes/proposalCreationTypes.ts +++ b/utils/uiTypes/proposalCreationTypes.ts @@ -385,6 +385,7 @@ export enum Instructions { VotingMintConfig, WithdrawObligationCollateralAndRedeemReserveLiquidity, WithdrawValidatorStake, + WithdrawFromVoteAccount, SplitStake, AddKeyToDID, RemoveKeyFromDID, @@ -468,6 +469,14 @@ export interface DelegateStakeForm { votePubkey: string } +export interface ValidatorWithdrawFromVoteAccountForm { + governedTokenAccount: AssetAccount | undefined + validatorVoteKey: string + authorizedWithdrawerKey: string + toPubkey: string + amount: number +} + export interface DualFinanceAirdropForm { root: string amount: number