diff --git a/apps/web/src/modules/create-dao/components/AllocationForm/AllocationForm.schema.ts b/apps/web/src/modules/create-dao/components/AllocationForm/AllocationForm.schema.ts index e6e4cd679..febbf6a42 100644 --- a/apps/web/src/modules/create-dao/components/AllocationForm/AllocationForm.schema.ts +++ b/apps/web/src/modules/create-dao/components/AllocationForm/AllocationForm.schema.ts @@ -3,7 +3,7 @@ import * as Yup from 'yup' import { isValidAddress } from 'src/utils/ens' import { getProvider } from 'src/utils/provider' -const allocationSchema = Yup.object().shape({ +export const allocationSchema = Yup.object().shape({ founderAddress: Yup.string() .test( 'isValidAddress', @@ -14,12 +14,12 @@ const allocationSchema = Yup.object().shape({ allocationPercentage: Yup.number() .transform((value) => (isNaN(value) ? undefined : value)) .required('*') + .integer('Must be whole number') + .max(100, '< 100') .when('admin', (admin, schema) => { if (!admin) return schema.min(1, '> 0') // (condition, errorMessage) - allocation represented as % must be greater than or equal to 0 return schema - }) - .max(100, '< 100') - .integer('Must be whole number'), + }), endDate: Yup.string() .required('*') .test('isDateInFuture', 'Must be in future', (value: string | undefined) => { diff --git a/apps/web/src/modules/dao/components/AdminForm/AdminForm.schema.ts b/apps/web/src/modules/dao/components/AdminForm/AdminForm.schema.ts index a5766ff70..84915c66f 100644 --- a/apps/web/src/modules/dao/components/AdminForm/AdminForm.schema.ts +++ b/apps/web/src/modules/dao/components/AdminForm/AdminForm.schema.ts @@ -1,7 +1,8 @@ import { Provider } from '@ethersproject/abstract-provider' import * as Yup from 'yup' -import { auctionSettingsValidationSchema } from 'src/modules/create-dao' +import { TokenAllocation, auctionSettingsValidationSchema } from 'src/modules/create-dao' +import { allocationSchema } from 'src/modules/create-dao/components/AllocationForm/AllocationForm.schema' import { Duration } from 'src/typings' import { isValidAddress } from 'src/utils/ens' import { durationValidationSchema, urlValidationSchema } from 'src/utils/yup' @@ -17,6 +18,7 @@ export interface AdminFormValues { quorumThreshold: number votingPeriod: Duration votingDelay: Duration + founderAllocation: TokenAllocation[] vetoPower: boolean vetoer: string } @@ -48,6 +50,16 @@ export const adminValidationSchema = (provider: Provider | undefined) => { value: tenMinutes, description: '10 minutes' }, { value: twentyFourWeeks, description: '24 weeks' } ), + founderAllocation: Yup.array() + .of(allocationSchema) + .test( + 'unique', + 'Founder allocation addresses should be unique.', + function (values) { + const addresses = values?.map((v) => v.founderAddress) + return values?.length === new Set(addresses)?.size + } + ), vetoPower: Yup.bool().required('*'), }) ) diff --git a/apps/web/src/modules/dao/components/AdminForm/AdminForm.tsx b/apps/web/src/modules/dao/components/AdminForm/AdminForm.tsx index fefa2c15b..3462e86ca 100644 --- a/apps/web/src/modules/dao/components/AdminForm/AdminForm.tsx +++ b/apps/web/src/modules/dao/components/AdminForm/AdminForm.tsx @@ -1,6 +1,6 @@ -import { Flex, Stack } from '@zoralabs/zord' -import { Contract, ethers } from 'ethers' -import { Formik, FormikValues } from 'formik' +import { Flex, Stack, Text } from '@zoralabs/zord' +import { BigNumber, Contract, ethers } from 'ethers' +import { FieldArray, Formik, FormikValues } from 'formik' import { AnimatePresence, motion } from 'framer-motion' import isEqual from 'lodash/isEqual' import { useRouter } from 'next/router' @@ -15,7 +15,8 @@ import TextArea from 'src/components/Fields/TextArea' import { NUMBER, TEXT } from 'src/components/Fields/types' import SingleImageUpload from 'src/components/SingleImageUpload/SingleImageUpload' import { NULL_ADDRESS } from 'src/constants/addresses' -import { auctionAbi, governorAbi, metadataAbi } from 'src/data/contract/abis' +import { auctionAbi, governorAbi, metadataAbi, tokenAbi } from 'src/data/contract/abis' +import { TokenAllocation } from 'src/modules/create-dao' import { BuilderTransaction, TransactionType, @@ -24,11 +25,14 @@ import { import { formValuesToTransactionMap } from 'src/modules/dao/utils/adminFormFieldToTransaction' import { useLayoutStore } from 'src/stores' import { sectionWrapperStyle } from 'src/styles/dao.css' +import { AddressType } from 'src/typings' import { getEnsAddress } from 'src/utils/ens' import { compareAndReturn, fromSeconds, unpackOptionalArray } from 'src/utils/helpers' import { DaoContracts, useDaoStore } from '../../stores' import { AdminFormValues, adminValidationSchema } from './AdminForm.schema' +import { AdminFounderAllocationFields } from './AdminFounderAllocationFields' +import { Section } from './Section' interface AdminFormProps { collectionAddress: string @@ -66,9 +70,15 @@ export const AdminForm: React.FC = ({ collectionAddress }) => { address: addresses?.metadata as Address, } + const tokenContractParams = { + abi: tokenAbi, + address: addresses?.token as Address, + } + const auctionContract = useContract(auctionContractParams) const governorContract = useContract(governorContractParams) const metadataContract = useContract(metadataContractParams) + const tokenContract = useContract(tokenContractParams) const { data } = useContractReads({ contracts: [ @@ -83,6 +93,7 @@ export const AdminForm: React.FC = ({ collectionAddress }) => { { ...metadataContractParams, functionName: 'projectURI' }, { ...metadataContractParams, functionName: 'rendererBase' }, { ...metadataContractParams, functionName: 'description' }, + { ...tokenContractParams, functionName: 'getFounders' }, ], }) @@ -98,12 +109,14 @@ export const AdminForm: React.FC = ({ collectionAddress }) => { daoWebsite, rendererBase, description, - ] = unpackOptionalArray(data, 11) + founders, + ] = unpackOptionalArray(data, 12) const contracts: DaoContracts = { auctionContract: auctionContract ?? undefined, governorContract: governorContract ?? undefined, metadataContract: metadataContract ?? undefined, + tokenContract: tokenContract ?? undefined, } const initialValues: AdminFormValues = { @@ -121,6 +134,12 @@ export const AdminForm: React.FC = ({ collectionAddress }) => { quorumThreshold: Number(quorumVotesBps) / 100 || 0, votingPeriod: fromSeconds(votingPeriod && Number(votingPeriod)), votingDelay: fromSeconds(votingDelay && Number(votingDelay)), + founderAllocation: + founders?.map((x) => ({ + founderAddress: x.wallet, + allocationPercentage: x.ownershipPct, + endDate: new Date(x.vestExpiry * 1000).toISOString(), + })) || [], vetoPower: !!vetoer && vetoer !== NULL_ADDRESS, vetoer: vetoer || '', @@ -189,6 +208,19 @@ export const AdminForm: React.FC = ({ collectionAddress }) => { value = await getEnsAddress(value as string, provider) } + if (field === 'founderAllocation') { + // @ts-ignore + value = (value as TokenAllocation[]).map( + ({ founderAddress, allocationPercentage, endDate }) => ({ + wallet: founderAddress as AddressType, + ownershipPct: allocationPercentage + ? BigNumber.from(allocationPercentage) + : BigNumber.from(0), + vestExpiry: BigNumber.from(Math.floor(new Date(endDate).getTime() / 1000)), + }) + ) + } + const transactionProperties = formValuesToTransactionMap[field] // @ts-ignore const calldata = transactionProperties.constructCalldata(contracts, value) @@ -253,180 +285,222 @@ export const AdminForm: React.FC = ({ collectionAddress }) => { validateOnMount > {(formik) => { - const changes = compareAndReturn(formik.initialValues, formik.values).length - + const founderChanges = isEqual( + formik.initialValues.founderAllocation, + formik.values.founderAllocation + ) + ? 0 + : 1 + const changes = + compareAndReturn(formik.initialValues, formik.values).length + + founderChanges return ( - - -