From 36308f00dd47ec4dc1df29be1f174d125cffafe1 Mon Sep 17 00:00:00 2001 From: Utkarsh <83659045+0xShuk@users.noreply.github.com> Date: Sat, 9 Nov 2024 21:49:22 +0530 Subject: [PATCH 1/2] add: multiple council members instruction --- hooks/useGovernanceAssets.ts | 5 + .../instructions/AddCouncilMembers.tsx | 160 ++++++++++++++++++ pages/dao/[symbol]/proposal/new.tsx | 2 + utils/uiTypes/proposalCreationTypes.ts | 8 + utils/validations.tsx | 48 ++++++ 5 files changed, 223 insertions(+) create mode 100644 pages/dao/[symbol]/proposal/components/instructions/AddCouncilMembers.tsx diff --git a/hooks/useGovernanceAssets.ts b/hooks/useGovernanceAssets.ts index f0f320b68..4f0e4fa11 100644 --- a/hooks/useGovernanceAssets.ts +++ b/hooks/useGovernanceAssets.ts @@ -337,6 +337,11 @@ export default function useGovernanceAssets() { isVisible: canUseTokenTransferInstruction, packageId: PackageEnum.Common, }, + [Instructions.AddCouncilMembers]: { + name: 'Add Council Members', + isVisible: canUseTokenTransferInstruction, + packageId: PackageEnum.Common, + }, [Instructions.TransferDomainName]: { name: 'SNS Transfer Out Domain Name', packageId: PackageEnum.Common, diff --git a/pages/dao/[symbol]/proposal/components/instructions/AddCouncilMembers.tsx b/pages/dao/[symbol]/proposal/components/instructions/AddCouncilMembers.tsx new file mode 100644 index 000000000..e3396aaf3 --- /dev/null +++ b/pages/dao/[symbol]/proposal/components/instructions/AddCouncilMembers.tsx @@ -0,0 +1,160 @@ +import React, { useContext, useEffect, useMemo, useState } from 'react' +import Input from '@components/inputs/Input' +import useRealm from '@hooks/useRealm' +import { PublicKey, TransactionInstruction } from '@solana/web3.js' +import { + AddCouncilMembersForm, + UiInstruction, +} from '@utils/uiTypes/proposalCreationTypes' +import { NewProposalContext } from '../../new' +import { getAddCouncilMembersSchema } from '@utils/validations' +import useGovernanceAssets from '@hooks/useGovernanceAssets' +import { serializeInstructionToBase64 } from '@solana/spl-governance' +import {validateInstruction} from '@utils/instructionTools' +import TextareaProps from '@components/inputs/Textarea' +import {withDepositGoverningTokens} from "@realms-today/spl-governance" +import { BN } from 'bn.js' +import useGoverningTokenMint from '@hooks/selectedRealm/useGoverningTokenMint' +import GovernedAccountSelect from '../GovernedAccountSelect' +import { useLegacyVoterWeight } from '@hooks/queries/governancePower' + +const AddCouncilMembers = ({ + index, +}: { + index: number +}) => { + const { realmInfo } = useRealm() + const communityMint = useGoverningTokenMint("community") + const { result: ownVoterWeight } = useLegacyVoterWeight() + const {assetAccounts} = useGovernanceAssets() + + const mintAssetAccount = useMemo(() => ( + communityMint ? + assetAccounts.find(account => account.pubkey.equals(communityMint)) : + undefined + ), [assetAccounts, communityMint]) + + const [form, setForm] = useState({ + governedTokenAccount: undefined, + amount: undefined, + memberAddresses: [], + mintInfo: undefined + }) + const [formErrors, setFormErrors] = useState({}) + + const { handleSetInstructions } = useContext(NewProposalContext) + + const handleSetForm = ({ propertyName, value }) => { + setFormErrors({}) + setForm({ ...form, [propertyName]: value }) + } + const setMintInfo = (value) => { + setForm({ ...form, mintInfo: value }) + } + const setAmount = (event) => { + const value = event.target.value + handleSetForm({ + value: value, + propertyName: 'amount', + }) + } + + function handleAddressChange(value: string) { + const addresses = value.split(",").map(a => a.trim()) + handleSetForm({ + value: addresses, + propertyName: 'memberAddresses' + }) + } + + async function getInstruction(): Promise { + const isValid = await validateInstruction({ schema, form, setFormErrors }) + + let serializedInstruction = '' + const additionalInstructions: string[] = [] + + if (form.amount && isValid && realmInfo && form.mintInfo && mintAssetAccount) { + for (const member of form.memberAddresses) { + const instructions: TransactionInstruction[] = [] + const amount = new BN(form.amount).mul(new BN(10**form.mintInfo.decimals)) + + await withDepositGoverningTokens( + instructions, + realmInfo.programId, + realmInfo.programVersion, + realmInfo.realmId, + mintAssetAccount.pubkey, + mintAssetAccount.pubkey, + new PublicKey(member), + form.mintInfo.mintAuthority!, + new PublicKey(member), + amount + ) + + if (serializedInstruction) { + additionalInstructions.push(serializeInstructionToBase64(instructions[0])) + } else { + serializedInstruction = serializeInstructionToBase64(instructions[0]) + } + } + } + + const obj = { + serializedInstruction, + additionalSerializedInstructions: additionalInstructions.length ? additionalInstructions : undefined, + isValid, + governance: form.governedTokenAccount?.governance, + chunkBy: 1 + } + + return obj + } + + useEffect(() => { + handleSetInstructions( + { governedAccount: form.governedTokenAccount?.governance, getInstruction }, + index + ) + }, [form]) + + useEffect(() => { + setMintInfo(mintAssetAccount?.extensions.mint?.account) + }, [mintAssetAccount]) + + const schema = getAddCouncilMembersSchema() + + return ( + <> + + ownVoterWeight?.canCreateProposal(x.governance.account.config) + )} + onChange={(value) => { + handleSetForm({ value, propertyName: 'governedTokenAccount' }) + }} + value={form.governedTokenAccount} + error={formErrors['governedTokenAccount']} + > + handleAddressChange(evt.target.value)} + error={formErrors['memberAddresses']} + className='h-40' + /> + + + + ) +} + +export default AddCouncilMembers diff --git a/pages/dao/[symbol]/proposal/new.tsx b/pages/dao/[symbol]/proposal/new.tsx index 30fe0ba64..d92c57747 100644 --- a/pages/dao/[symbol]/proposal/new.tsx +++ b/pages/dao/[symbol]/proposal/new.tsx @@ -146,6 +146,7 @@ import SymmetryEditBasket from './components/instructions/Symmetry/SymmetryEditB import SymmetryDeposit from './components/instructions/Symmetry/SymmetryDeposit' import SymmetryWithdraw from './components/instructions/Symmetry/SymmetryWithdraw' import PythUpdatePoolAuthority from './components/instructions/Pyth/PythUpdatePoolAuthority' +import AddCouncilMembers from './components/instructions/AddCouncilMembers' 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) @@ -469,6 +470,7 @@ const New = () => { | null } = useMemo( () => ({ + [Instructions.AddCouncilMembers]: AddCouncilMembers, [Instructions.Burn]: BurnTokens, [Instructions.Transfer]: SplTokenTransfer, [Instructions.ProgramUpgrade]: ProgramUpgrade, diff --git a/utils/uiTypes/proposalCreationTypes.ts b/utils/uiTypes/proposalCreationTypes.ts index d0aaa9eaa..e55b41a66 100644 --- a/utils/uiTypes/proposalCreationTypes.ts +++ b/utils/uiTypes/proposalCreationTypes.ts @@ -50,6 +50,13 @@ export interface SplTokenTransferForm { mintInfo: MintInfo | undefined } +export interface AddCouncilMembersForm { + governedTokenAccount: AssetAccount | undefined, + memberAddresses: string[], + amount: number | undefined, + mintInfo: MintInfo | undefined +} + export interface BurnTokensForm { amount: number | undefined governedTokenAccount: AssetAccount | undefined @@ -295,6 +302,7 @@ export interface JoinDAOForm { } export enum Instructions { + AddCouncilMembers, Base64, Burn, ChangeMakeDonation, diff --git a/utils/validations.tsx b/utils/validations.tsx index 7f9c81817..ed369db41 100644 --- a/utils/validations.tsx +++ b/utils/validations.tsx @@ -881,6 +881,54 @@ export const getDualFinanceVoteDepositSchema = () => { }) } +export const getAddCouncilMembersSchema = () => { + return yup.object().shape({ + governedTokenAccount: yup.object().required('Source account is required'), + amount: yup.number() + .typeError('Amount is required') + .min(0) + .test( + 'amount', + 'Amount validation error', + function (val: number) { + if (new BN(val).gt(new BN(0))) { + return true + } + return this.createError({ + message: `Amount is required`, + }) + } + ), + memberAddresses: yup + .array().min(1).of( + yup + .string() + .typeError('Address is required') + .test( + 'memberAddresses', + 'Account validation error', + function (val: string) { + if (val) { + try { + getValidatedPublickKey(val) + return true + } catch (e) { + console.log(e) + return this.createError({ + message: `Invalid Public Key`, + }) + } + } else { + return this.createError({ + message: `Member addresses are required`, + }) + } + } + ) + ), + }) +} + export const getTokenTransferSchema = ({ form, connection, From b78280bac95e8f210a40bf1f39ffa9dbd3f0e8e4 Mon Sep 17 00:00:00 2001 From: Utkarsh <83659045+0xShuk@users.noreply.github.com> Date: Sat, 9 Nov 2024 22:48:50 +0530 Subject: [PATCH 2/2] adjust textbox height --- .../proposal/components/instructions/AddCouncilMembers.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/dao/[symbol]/proposal/components/instructions/AddCouncilMembers.tsx b/pages/dao/[symbol]/proposal/components/instructions/AddCouncilMembers.tsx index e3396aaf3..95990dfa7 100644 --- a/pages/dao/[symbol]/proposal/components/instructions/AddCouncilMembers.tsx +++ b/pages/dao/[symbol]/proposal/components/instructions/AddCouncilMembers.tsx @@ -142,7 +142,7 @@ const AddCouncilMembers = ({ type="textarea" onChange={(evt) => handleAddressChange(evt.target.value)} error={formErrors['memberAddresses']} - className='h-40' + className='h-28' />