From 495f2f9663c518feec4e38641441df3127dac72e Mon Sep 17 00:00:00 2001 From: neokry Date: Thu, 23 Mar 2023 14:57:48 -0700 Subject: [PATCH 1/9] Add founders allocation to about page --- .../modules/dao/components/About/About.tsx | 23 ++++++++--- .../modules/dao/components/About/Founder.tsx | 41 +++++++++++++++++++ 2 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 apps/web/src/modules/dao/components/About/Founder.tsx diff --git a/apps/web/src/modules/dao/components/About/About.tsx b/apps/web/src/modules/dao/components/About/About.tsx index 400af4c41..2c285903b 100644 --- a/apps/web/src/modules/dao/components/About/About.tsx +++ b/apps/web/src/modules/dao/components/About/About.tsx @@ -1,4 +1,4 @@ -import { Box, Flex, Text } from '@zoralabs/zord' +import { Box, Flex, Grid, Text } from '@zoralabs/zord' import HtmlReactParser from 'html-react-parser' import { getFetchableUrl } from 'ipfs-service' import Image from 'next/legacy/image' @@ -18,6 +18,7 @@ import { formatCryptoVal } from 'src/utils/numbers' import { useDaoStore } from '../../stores' import { parseContractURI } from '../../utils' import { ExternalLinks } from './ExternalLinks' +import { Founder } from './Founder' import { Statistic } from './Statistic' export const About: React.FC = () => { @@ -38,16 +39,15 @@ export const About: React.FC = () => { contracts: [ { ...tokenContractParams, functionName: 'name' }, { ...tokenContractParams, functionName: 'totalSupply' }, + { ...tokenContractParams, functionName: 'getFounders' }, { ...metadataContractParams, functionName: 'contractImage' }, { ...metadataContractParams, functionName: 'description' }, { ...metadataContractParams, functionName: 'contractURI' }, ], }) - const [name, totalSupply, daoImage, description, contractURI] = unpackOptionalArray( - contractData, - 5 - ) + const [name, totalSupply, founders, daoImage, description, contractURI] = + unpackOptionalArray(contractData, 6) const parsedContractURI = parseContractURI(contractURI) const { data: balance } = useBalance({ address: treasury as Address }) @@ -127,6 +127,19 @@ export const About: React.FC = () => { ) : null} + {typeof founders !== 'undefined' && founders.length > 0 ? ( + <> + + Founders + + + {founders.map((founder) => ( + + ))} + + + ) : null} + = ({ wallet, ownershipPct }) => { + const { displayName, ensAvatar } = useEnsData(wallet as string) + return ( + + + + + {displayName} + + + {ownershipPct}% + + ) +} From fa240bfbbaf3f25066b662496fea7dd1241f4b57 Mon Sep 17 00:00:00 2001 From: neokry Date: Thu, 23 Mar 2023 15:26:05 -0700 Subject: [PATCH 2/9] Fix layout for mobile --- .../src/modules/dao/components/About/About.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/web/src/modules/dao/components/About/About.tsx b/apps/web/src/modules/dao/components/About/About.tsx index 2c285903b..ae29a2c9c 100644 --- a/apps/web/src/modules/dao/components/About/About.tsx +++ b/apps/web/src/modules/dao/components/About/About.tsx @@ -11,6 +11,7 @@ import { CHAIN } from 'src/constants/network' import SWR_KEYS from 'src/constants/swrKeys' import { metadataAbi, tokenAbi } from 'src/data/contract/abis' import { sdk } from 'src/data/graphql/client' +import { useLayoutStore } from 'src/stores' import { about, daoDescription, daoInfo, daoName } from 'src/styles/About.css' import { unpackOptionalArray } from 'src/utils/helpers' import { formatCryptoVal } from 'src/utils/numbers' @@ -25,6 +26,7 @@ export const About: React.FC = () => { const { addresses: { token, treasury, metadata }, } = useDaoStore() + const { isMobile } = useLayoutStore() const tokenContractParams = { abi: tokenAbi, @@ -127,25 +129,25 @@ export const About: React.FC = () => { ) : null} + + + + {typeof founders !== 'undefined' && founders.length > 0 ? ( <> Founders - + {founders.map((founder) => ( ))} ) : null} - - - - ) } From 950de286e14e44872462a93b2b175aa6c49f643a Mon Sep 17 00:00:00 2001 From: neokry Date: Fri, 24 Mar 2023 12:05:06 -0700 Subject: [PATCH 3/9] Add vesting tooltip to founder component --- .../src/components/Icon/assets/info-16.svg | 11 ++++++++++ apps/web/src/components/Icon/icons.ts | 2 ++ .../modules/dao/components/About/Founder.tsx | 20 ++++++++++++++++--- 3 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/components/Icon/assets/info-16.svg diff --git a/apps/web/src/components/Icon/assets/info-16.svg b/apps/web/src/components/Icon/assets/info-16.svg new file mode 100644 index 000000000..743c277a2 --- /dev/null +++ b/apps/web/src/components/Icon/assets/info-16.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/apps/web/src/components/Icon/icons.ts b/apps/web/src/components/Icon/icons.ts index f82ce104b..26d8ddb1d 100644 --- a/apps/web/src/components/Icon/icons.ts +++ b/apps/web/src/components/Icon/icons.ts @@ -22,6 +22,7 @@ import External from './assets/external-16.svg' import Github from './assets/github.svg' import Globe from './assets/globe.svg' import HandlebarCircle from './assets/handlebar-circle.svg' +import Info16 from './assets/info-16.svg' import Move from './assets/move.svg' import NewWindow from './assets/new-window.svg' import NounsConnect from './assets/nouns-connect.svg' @@ -56,6 +57,7 @@ export const icons = { github: Github, eth: Eth, handlebarCircle: HandlebarCircle, + 'info-16': Info16, globe: Globe, move: Move, newWindow: NewWindow, diff --git a/apps/web/src/modules/dao/components/About/Founder.tsx b/apps/web/src/modules/dao/components/About/Founder.tsx index 18fe88de1..16e26b4ce 100644 --- a/apps/web/src/modules/dao/components/About/Founder.tsx +++ b/apps/web/src/modules/dao/components/About/Founder.tsx @@ -1,6 +1,7 @@ -import { Flex, Text } from '@zoralabs/zord' +import { Box, Flex, Text } from '@zoralabs/zord' import { Avatar } from 'src/components/Avatar' +import { Icon } from 'src/components/Icon' import { ETHERSCAN_BASE_URL } from 'src/constants/etherscan' import { useEnsData } from 'src/hooks' import { AddressType } from 'src/typings' @@ -8,10 +9,16 @@ import { AddressType } from 'src/typings' interface FounderProps { wallet: AddressType ownershipPct: number + vestExpiry: number } -export const Founder: React.FC = ({ wallet, ownershipPct }) => { +export const Founder: React.FC = ({ wallet, ownershipPct, vestExpiry }) => { const { displayName, ensAvatar } = useEnsData(wallet as string) + const vestDate = new Date(vestExpiry * 1000).toLocaleDateString(undefined, { + year: 'numeric', + month: 'short', + day: 'numeric', + }) return ( = ({ wallet, ownershipPct }) => { {displayName} - {ownershipPct}% + + + {ownershipPct}% + + + + + ) } From fcd0b70fb488ec3fc5ddc2837a0ebc01a6128b9e Mon Sep 17 00:00:00 2001 From: neokry Date: Fri, 24 Mar 2023 14:52:42 -0700 Subject: [PATCH 4/9] Add edit founder controls to DAO admin section --- .../AllocationForm/AllocationForm.schema.ts | 2 +- .../components/AdminForm/AdminForm.schema.ts | 14 +- .../dao/components/AdminForm/AdminForm.tsx | 67 +++++++- .../AdminFounderAllocationFields.tsx | 149 ++++++++++++++++++ .../dao/utils/adminFormFieldToTransaction.tsx | 9 ++ 5 files changed, 233 insertions(+), 8 deletions(-) create mode 100644 apps/web/src/modules/dao/components/AdminForm/AdminFounderAllocationFields.tsx 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 886818cae..9aa11c969 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', 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..214756feb 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 { 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,13 @@ 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' interface AdminFormProps { collectionAddress: string @@ -66,9 +69,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 +92,7 @@ export const AdminForm: React.FC = ({ collectionAddress }) => { { ...metadataContractParams, functionName: 'projectURI' }, { ...metadataContractParams, functionName: 'rendererBase' }, { ...metadataContractParams, functionName: 'description' }, + { ...tokenContractParams, functionName: 'getFounders' }, ], }) @@ -98,12 +108,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 +133,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 +207,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,8 +284,15 @@ 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 ( @@ -390,6 +428,23 @@ export const AdminForm: React.FC = ({ collectionAddress }) => { onBlur={formik.handleBlur} errorMessage={formik.errors['votingDelay']} /> + + + {({ remove, push }) => ( + + push({ founderAddress: '', allocation: '', endDate: '' }) + } + /> + )} + + + touched: FormikTouched + formik: FormikProps + removeFounderAddress: (index: number) => void + addFounderAddress: () => void +} + +export const AdminFounderAllocationFields = ({ + values, + auctionDuration, + errors, + touched, + formik, + removeFounderAddress, + addFounderAddress, +}: AdminFounderAllocationFieldsProps) => { + return ( + + + {values.founderAllocation.map((founder, index) => { + const error = + typeof errors?.founderAllocation === 'object' + ? (errors?.founderAllocation?.[index] as FormikErrors) + : undefined + + const touchedField = touched?.founderAllocation?.[index] + + return ( + + + + + + + + + + + + + + + + + + + {founder?.allocationPercentage && founder?.endDate ? ( + + ~{' '} + {calculateMaxAllocation( + founder?.allocationPercentage, + founder?.endDate, + auctionDuration + )}{' '} + Tokens + + ) : null} + + + ) + })} + + + + + + + {typeof errors?.founderAllocation === 'string' && ( + + {errors?.founderAllocation} + + )} + + ) +} diff --git a/apps/web/src/modules/dao/utils/adminFormFieldToTransaction.tsx b/apps/web/src/modules/dao/utils/adminFormFieldToTransaction.tsx index 86614e0d9..5d3b639d7 100644 --- a/apps/web/src/modules/dao/utils/adminFormFieldToTransaction.tsx +++ b/apps/web/src/modules/dao/utils/adminFormFieldToTransaction.tsx @@ -106,6 +106,15 @@ export const formValuesToTransactionMap: FormValuesTransactionMap = { toSeconds(value), ]), }, + founderAllocation: { + functionSignature: 'updateFounders', + getTarget: (addresses) => addresses.token as AddressType, + constructCalldata: ({ tokenContract }, value) => + tokenContract?.interface.encodeFunctionData( + 'updateFounders((address,uint256,uint256)[])', + [value] + ), + }, vetoPower: { functionSignature: 'burnVetoer', getTarget: (addresses) => addresses.governor as AddressType, From f13b1e74b4106f1c66155882c19369bdb8ae1659 Mon Sep 17 00:00:00 2001 From: neokry Date: Fri, 24 Mar 2023 14:53:53 -0700 Subject: [PATCH 5/9] Remove unused components --- .../dao/components/AdminForm/AdminFounderAllocationFields.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/modules/dao/components/AdminForm/AdminFounderAllocationFields.tsx b/apps/web/src/modules/dao/components/AdminForm/AdminFounderAllocationFields.tsx index 8833ccee8..c2dd91489 100644 --- a/apps/web/src/modules/dao/components/AdminForm/AdminFounderAllocationFields.tsx +++ b/apps/web/src/modules/dao/components/AdminForm/AdminFounderAllocationFields.tsx @@ -1,4 +1,4 @@ -import { Button, Flex, Heading, Paragraph, Stack, Text } from '@zoralabs/zord' +import { Button, Flex, Stack, Text } from '@zoralabs/zord' import { FormikErrors, FormikProps, FormikTouched } from 'formik' import React from 'react' From 36fa62ac62cb3904511de25c4a36024c5c628d11 Mon Sep 17 00:00:00 2001 From: neokry Date: Fri, 24 Mar 2023 15:42:01 -0700 Subject: [PATCH 6/9] Change link to address --- .../modules/dao/components/About/Founder.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/apps/web/src/modules/dao/components/About/Founder.tsx b/apps/web/src/modules/dao/components/About/Founder.tsx index 16e26b4ce..f205c1970 100644 --- a/apps/web/src/modules/dao/components/About/Founder.tsx +++ b/apps/web/src/modules/dao/components/About/Founder.tsx @@ -21,10 +21,6 @@ export const Founder: React.FC = ({ wallet, ownershipPct, vestExpi }) return ( = ({ wallet, ownershipPct, vestExpi p="x4" px="x6" > - + - {displayName} + + {displayName} + {ownershipPct}% - + From 4841a87d060d3e0966858238398721380791c6db Mon Sep 17 00:00:00 2001 From: neokry Date: Fri, 24 Mar 2023 16:26:36 -0700 Subject: [PATCH 7/9] Fix tooltop for mobile --- .../src/modules/dao/components/About/Founder.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/apps/web/src/modules/dao/components/About/Founder.tsx b/apps/web/src/modules/dao/components/About/Founder.tsx index f205c1970..2f2dc36e0 100644 --- a/apps/web/src/modules/dao/components/About/Founder.tsx +++ b/apps/web/src/modules/dao/components/About/Founder.tsx @@ -1,4 +1,5 @@ -import { Box, Flex, Text } from '@zoralabs/zord' +import { Box, Flex, PopUp, Text } from '@zoralabs/zord' +import { useState } from 'react' import { Avatar } from 'src/components/Avatar' import { Icon } from 'src/components/Icon' @@ -13,6 +14,7 @@ interface FounderProps { } export const Founder: React.FC = ({ wallet, ownershipPct, vestExpiry }) => { + const [showTooltip, setShowTooltip] = useState(false) const { displayName, ensAvatar } = useEnsData(wallet as string) const vestDate = new Date(vestExpiry * 1000).toLocaleDateString(undefined, { year: 'numeric', @@ -50,9 +52,17 @@ export const Founder: React.FC = ({ wallet, ownershipPct, vestExpi {ownershipPct}% - + setShowTooltip(true)} + onMouseLeave={() => setShowTooltip(false)} + > + }> + {`In effect until ${vestDate}`} + ) From 08274544aa5cbe11581b708034516ba38ffb2a36 Mon Sep 17 00:00:00 2001 From: neokry Date: Mon, 27 Mar 2023 13:23:38 -0700 Subject: [PATCH 8/9] Adds sections to admin form --- .../AllocationForm/AllocationForm.schema.ts | 4 +- .../dao/components/AdminForm/AdminForm.tsx | 396 ++++++++++-------- .../AdminFounderAllocationFields.tsx | 4 +- .../dao/components/AdminForm/Section.tsx | 28 ++ 4 files changed, 244 insertions(+), 188 deletions(-) create mode 100644 apps/web/src/modules/dao/components/AdminForm/Section.tsx 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 9aa11c969..90a70d83a 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 @@ -14,9 +14,9 @@ export const allocationSchema = Yup.object().shape({ allocationPercentage: Yup.number() .transform((value) => (isNaN(value) ? undefined : value)) .required('*') + .integer('Must be whole number') .min(1, '> 0') // (condition, errorMessage) - allocation represented as % must be greater than or equal to 0 - .max(100, '< 100') - .integer('Must be whole number'), + .max(100, '< 100'), endDate: Yup.string() .required('*') .test('isDateInFuture', 'Must be in future', (value: string | undefined) => { diff --git a/apps/web/src/modules/dao/components/AdminForm/AdminForm.tsx b/apps/web/src/modules/dao/components/AdminForm/AdminForm.tsx index 214756feb..9a46d29df 100644 --- a/apps/web/src/modules/dao/components/AdminForm/AdminForm.tsx +++ b/apps/web/src/modules/dao/components/AdminForm/AdminForm.tsx @@ -1,4 +1,4 @@ -import { Flex, Stack } from '@zoralabs/zord' +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' @@ -32,6 +32,7 @@ import { compareAndReturn, fromSeconds, unpackOptionalArray } from 'src/utils/he import { DaoContracts, useDaoStore } from '../../stores' import { AdminFormValues, adminValidationSchema } from './AdminForm.schema' import { AdminFounderAllocationFields } from './AdminFounderAllocationFields' +import { Section } from './Section' interface AdminFormProps { collectionAddress: string @@ -296,192 +297,219 @@ export const AdminForm: React.FC = ({ collectionAddress }) => { return ( - - -