Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add proposal type for solana withdraw-from-vote-account for a DAO #2471

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions hooks/useGovernanceAssets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Governance> | null
}) => {
const connection = useLegacyConnectionContext()
const programId: PublicKey = StakeProgram.programId
const { governedTokenAccountsWithoutNfts } = useGovernanceAssets()
const shouldBeGoverned = !!(index !== 0 && governance)
const wallet = useWalletOnePointOh()

const [form, setForm] = useState<ValidatorWithdrawFromVoteAccountForm>({
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<Governance> | 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<boolean> => {
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<UiInstruction> {
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 (
<>
<GovernedAccountSelect
label="Source account"
governedAccounts={governedTokenAccountsWithoutNfts.filter(
(x) => x.isSol
)}
onChange={(value) => {
handleSetForm({ value, propertyName: 'governedTokenAccount' })
}}
value={form.governedTokenAccount}
error={formErrors['governedTokenAccount']}
shouldBeGoverned={shouldBeGoverned}
governance={governance}
type="token"
></GovernedAccountSelect>
<Input
label="Validator Vote Address"
value={form.validatorVoteKey}
error={formErrors['validatorVoteKey']}
type="string"
onChange={setValidatorVoteKey}
/>
<Input
label="Authorized Withdrawer Key (Should be DAO wallet)"
value={form.authorizedWithdrawerKey}
error={formErrors['authorizedWithdrawerKey']}
type="string"
onChange={setAuthorizedWithdrawerKey}
/>
<Input
label="toPubkey (where the SOL is going, should be DAO wallet)"
value={form.toPubkey}
error={formErrors['toPubkey']}
type="string"
onChange={setToPubkey}
/>
<Input
label="Amount"
value={form.amount}
error={formErrors['amount']}
type="number"
min="1"
onChange={setAmount}
/>
<div
style={{
fontSize: '14px',
color: 'rgba(164, 172, 183, 1)',
marginTop: '18px',
}}
>
Withdraw SOL from vote account. This uses withdraw and not safeWithdraw, so check that the rent exempt amount remains.
</div>
</>
)
}

export default WithdrawFromVoteAccount
2 changes: 2 additions & 0 deletions pages/dao/[symbol]/proposal/new.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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,
Expand Down
9 changes: 9 additions & 0 deletions utils/uiTypes/proposalCreationTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,7 @@ export enum Instructions {
VotingMintConfig,
WithdrawObligationCollateralAndRedeemReserveLiquidity,
WithdrawValidatorStake,
WithdrawFromVoteAccount,
SplitStake,
AddKeyToDID,
RemoveKeyFromDID,
Expand Down Expand Up @@ -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
Expand Down
Loading