diff --git a/packages/i18n/locales/en/translation.json b/packages/i18n/locales/en/translation.json index c18ac7d17..c0fd5d38b 100644 --- a/packages/i18n/locales/en/translation.json +++ b/packages/i18n/locales/en/translation.json @@ -307,6 +307,7 @@ "counterpartyBalanceInsufficient": "The counterparty's balance of {{amount}} ${{tokenSymbol}} is insufficient. They may be unable to complete this swap.", "daoAccountNotFound": "The DAO account could not be found for this chain.", "daoAndSubDaosAlreadyOnV2": "This DAO (and all of its SubDAOs, if it has any) have already been upgraded.", + "daoCreationIncomplete": "DAO creation is incomplete. Ensure all required fields have been filled out.", "daoFeatureUnsupported": "{{name}} does not support {{feature}} yet.", "daoIsInactive_absolute_one": "This DAO is inactive. Proposals cannot be created until {{count}} token is staked.", "daoIsInactive_absolute_other": "This DAO is inactive. Proposals cannot be created until {{count}} tokens are staked.", diff --git a/packages/stateful/components/dao/CreateDaoForm.tsx b/packages/stateful/components/dao/CreateDaoForm.tsx index 4664032ca..f7a41e6c3 100644 --- a/packages/stateful/components/dao/CreateDaoForm.tsx +++ b/packages/stateful/components/dao/CreateDaoForm.tsx @@ -3,7 +3,7 @@ import { Buffer } from 'buffer' import { ArrowBack } from '@mui/icons-material' import cloneDeep from 'lodash.clonedeep' import merge from 'lodash.merge' -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { SubmitErrorHandler, SubmitHandler, useForm } from 'react-hook-form' import toast from 'react-hot-toast' import { useTranslation } from 'react-i18next' @@ -162,9 +162,8 @@ export const InnerCreateDaoForm = ({ // Merge defaults in case there are any new fields. const creator = getCreatorById(cached.creator.id) - merge( - // Merges into this object. - cached.creator.data, + cached.creator.data = merge( + {}, // Start with defaults. creator?.defaultConfig, // Overwrite with existing values. @@ -173,9 +172,8 @@ export const InnerCreateDaoForm = ({ cached.proposalModuleAdapters?.forEach((adapter) => { const proposalModuleAdapter = getProposalModuleAdapterById(adapter.id) - merge( - // Merges into this object. - adapter.data, + adapter.data = merge( + {}, // Start with defaults. proposalModuleAdapter?.daoCreation?.extraVotingConfig?.default, // Overwrite with existing values. @@ -187,9 +185,8 @@ export const InnerCreateDaoForm = ({ if (!cached.votingConfig) { cached.votingConfig = defaultNewDao.votingConfig } - merge( - // Merge into this object. - cached.votingConfig, + cached.votingConfig = merge( + {}, // Start with defaults. defaultNewDao.votingConfig, // Overwrite with existing values. @@ -311,8 +308,9 @@ export const InnerCreateDaoForm = ({ [t] ) - // Generate instantiation message. - const generateInstantiateMsg = useCallback(() => { + let instantiateMsg: DaoCoreV2InstantiateMsg | undefined + let instantiateMsgError: string | undefined + try { // Generate proposal module adapters' instantiation messages. const proposalModuleInstantiateInfos = proposalModuleDaoCreationAdapters.map(({ getInstantiateInfo }, index) => @@ -324,7 +322,7 @@ export const InnerCreateDaoForm = ({ ) ) - let instantiateMsg: DaoCoreV2InstantiateMsg = { + instantiateMsg = { // If parentDao exists, let's make a subDAO :D admin: parentDao?.coreAddress ?? null, automatically_add_cw20s: true, @@ -354,22 +352,12 @@ export const InnerCreateDaoForm = ({ // Validate and throw error if invalid according to JSON schema. validateInstantiateMsg(instantiateMsg) + } catch (err) { + instantiateMsgError = err instanceof Error ? err.message : `${err}` + } - return instantiateMsg - }, [ - proposalModuleDaoCreationAdapters, - parentDao?.coreAddress, - description, - imageUrl, - name, - creator, - newDao, - creatorData, - t, - codeIds, - validateInstantiateMsg, - proposalModuleAdapters, - ]) + const instantiateMsgFunds = + instantiateMsg && getFundsFromDaoInstantiateMsg(instantiateMsg) //! Submit handlers @@ -383,35 +371,34 @@ export const InnerCreateDaoForm = ({ sender: walletAddress ?? '', }) - const createDaoWithFactory = useCallback(async () => { - const cwCoreInstantiateMsg = generateInstantiateMsg() + const createDaoWithFactory = async () => { + if (instantiateMsgError) { + throw new Error(instantiateMsgError) + } else if (!instantiateMsg) { + throw new Error(t('error.loadingData')) + } const { logs } = await instantiateWithFactory( { codeId: codeIds.DaoCore, instantiateMsg: Buffer.from( - JSON.stringify(cwCoreInstantiateMsg), + JSON.stringify(instantiateMsg), 'utf8' ).toString('base64'), - label: cwCoreInstantiateMsg.name, + label: instantiateMsg.name, }, CHAIN_GAS_MULTIPLIER, undefined, - getFundsFromDaoInstantiateMsg(cwCoreInstantiateMsg) + getFundsFromDaoInstantiateMsg(instantiateMsg) ) return findWasmAttributeValue( logs, factoryContractAddress, 'set contract admin as itself' )! - }, [ - codeIds.DaoCore, - factoryContractAddress, - generateInstantiateMsg, - instantiateWithFactory, - ]) - - const parseSubmitterValueDelta = useCallback((value: string): number => { + } + + const parseSubmitterValueDelta = (value: string): number => { switch (value) { case CreateDaoSubmitValue.Back: return -1 @@ -425,7 +412,7 @@ export const InnerCreateDaoForm = ({ return 0 } - }, []) + } const [customValidator, setCustomValidator] = useState() @@ -436,245 +423,191 @@ export const InnerCreateDaoForm = ({ : undefined const awaitNextBlock = useAwaitNextBlock() - const onSubmit: SubmitHandler = useCallback( - async (values, event) => { - // If navigating, no need to display errors. - form.clearErrors() - - const nativeEvent = event?.nativeEvent as SubmitEvent - const submitterValue = (nativeEvent?.submitter as HTMLInputElement)?.value - - // Create the DAO. - if (submitterValue === CreateDaoSubmitValue.Create) { - if (isWalletConnected) { - setCreating(true) - try { - const coreAddress = await toast.promise(createDaoWithFactory(), { - loading: t('info.creatingDao'), - success: t('success.daoCreatedPleaseWait'), - error: (err) => processError(err), - }) - - // Don't set following on SDA. Only dApp. - if (mode !== DaoPageMode.Sda) { - setFollowing(coreAddress) - } - - // New wallet balances will not appear until the next block. - awaitNextBlock().then(refreshBalances) - - //! Show DAO created modal. - - const nativeToken = getNativeTokenForChainId(chainId) - - // Get tokenSymbol and tokenBalance for DAO card. - const { tokenSymbol, tokenBalance, tokenDecimals } = - creatorId === TokenBasedCreatorId && - daoVotingTokenBasedCreatorData - ? //! Display governance token supply if using governance tokens. - { - tokenBalance: - daoVotingTokenBasedCreatorData.tokenType === - GovernanceTokenType.New - ? daoVotingTokenBasedCreatorData.newInfo.initialSupply - : // If using existing token but no token info loaded (should - // be impossible), just display 0. - !daoVotingTokenBasedCreatorData.existingToken || - daoVotingTokenBasedCreatorData.existingTokenSupply === - undefined - ? 0 - : // If using existing token, convert supply from query using decimals. - convertMicroDenomToDenomWithDecimals( - daoVotingTokenBasedCreatorData.existingTokenSupply, - daoVotingTokenBasedCreatorData.existingToken - .decimals - ), - tokenSymbol: - daoVotingTokenBasedCreatorData.tokenType === - GovernanceTokenType.New - ? daoVotingTokenBasedCreatorData.newInfo.symbol - : // If using existing token but no token info loaded (should - // be impossible), the tokenBalance above will be set to - // 0, so use the native token here so this value is - // accurate. - !daoVotingTokenBasedCreatorData.existingToken - ? nativeToken.symbol - : daoVotingTokenBasedCreatorData.existingToken.symbol || - t('info.token').toLocaleUpperCase(), - tokenDecimals: - daoVotingTokenBasedCreatorData.tokenType === - GovernanceTokenType.Existing && - daoVotingTokenBasedCreatorData.existingToken - ? daoVotingTokenBasedCreatorData.existingToken.decimals - : // If using existing token but no token info loaded - // (should be impossible), the tokenBalance above will - // be set to 0, so it doesn't matter that this is - // wrong. - NEW_DAO_TOKEN_DECIMALS, - } - : //! Otherwise display native token, which has a balance of 0 initially. - { - tokenBalance: 0, - tokenSymbol: nativeToken.symbol, - tokenDecimals: nativeToken.decimals, - } - - // Set card props to show modal. - setDaoCreatedCardProps({ - chainId, - coreAddress, - name, - description, - imageUrl: imageUrl || getFallbackImage(coreAddress), - polytoneProxies: {}, - established: new Date(), - showIsMember: false, - parentDao, - tokenDecimals, - tokenSymbol, - showingEstimatedUsdValue: false, - lazyData: { - loading: false, - data: { - tokenBalance, - // Does not matter, will not show. - isMember: false, - proposalCount: 0, - }, - }, - }) - - // Clear saved form data. - setNewDaoAtom(makeDefaultNewDao(chainId)) - - // Navigate to DAO page (underneath the creation modal). - goToDao(coreAddress) - } catch (err) { - // toast.promise above will handle displaying the error - console.error(err) - setCreating(false) + const onSubmit: SubmitHandler = async (values, event) => { + // If navigating, no need to display errors. + form.clearErrors() + + const nativeEvent = event?.nativeEvent as SubmitEvent + const submitterValue = (nativeEvent?.submitter as HTMLInputElement)?.value + + // Create the DAO. + if (submitterValue === CreateDaoSubmitValue.Create) { + if (isWalletConnected) { + setCreating(true) + try { + const coreAddress = await toast.promise(createDaoWithFactory(), { + loading: t('info.creatingDao'), + success: t('success.daoCreatedPleaseWait'), + error: (err) => processError(err), + }) + + // Don't set following on SDA. Only dApp. + if (mode !== DaoPageMode.Sda) { + setFollowing(coreAddress) } - // Don't stop creating on success, since we are navigating to a new - // page and want to prevent creating duplicate DAOs. - } else { - toast.error(t('error.logInToCreate')) - } - return + // New wallet balances will not appear until the next block. + awaitNextBlock().then(refreshBalances) + + //! Show DAO created modal. + + const nativeToken = getNativeTokenForChainId(chainId) + + // Get tokenSymbol and tokenBalance for DAO card. + const { tokenSymbol, tokenBalance, tokenDecimals } = + creatorId === TokenBasedCreatorId && daoVotingTokenBasedCreatorData + ? //! Display governance token supply if using governance tokens. + { + tokenBalance: + daoVotingTokenBasedCreatorData.tokenType === + GovernanceTokenType.New + ? daoVotingTokenBasedCreatorData.newInfo.initialSupply + : // If using existing token but no token info loaded (should + // be impossible), just display 0. + !daoVotingTokenBasedCreatorData.existingToken || + daoVotingTokenBasedCreatorData.existingTokenSupply === + undefined + ? 0 + : // If using existing token, convert supply from query using decimals. + convertMicroDenomToDenomWithDecimals( + daoVotingTokenBasedCreatorData.existingTokenSupply, + daoVotingTokenBasedCreatorData.existingToken.decimals + ), + tokenSymbol: + daoVotingTokenBasedCreatorData.tokenType === + GovernanceTokenType.New + ? daoVotingTokenBasedCreatorData.newInfo.symbol + : // If using existing token but no token info loaded (should + // be impossible), the tokenBalance above will be set to + // 0, so use the native token here so this value is + // accurate. + !daoVotingTokenBasedCreatorData.existingToken + ? nativeToken.symbol + : daoVotingTokenBasedCreatorData.existingToken.symbol || + t('info.token').toLocaleUpperCase(), + tokenDecimals: + daoVotingTokenBasedCreatorData.tokenType === + GovernanceTokenType.Existing && + daoVotingTokenBasedCreatorData.existingToken + ? daoVotingTokenBasedCreatorData.existingToken.decimals + : // If using existing token but no token info loaded + // (should be impossible), the tokenBalance above will + // be set to 0, so it doesn't matter that this is + // wrong. + NEW_DAO_TOKEN_DECIMALS, + } + : //! Otherwise display native token, which has a balance of 0 initially. + { + tokenBalance: 0, + tokenSymbol: nativeToken.symbol, + tokenDecimals: nativeToken.decimals, + } + + // Set card props to show modal. + setDaoCreatedCardProps({ + chainId, + coreAddress, + name, + description, + imageUrl: imageUrl || getFallbackImage(coreAddress), + polytoneProxies: {}, + established: new Date(), + showIsMember: false, + parentDao, + tokenDecimals, + tokenSymbol, + showingEstimatedUsdValue: false, + lazyData: { + loading: false, + data: { + tokenBalance, + // Does not matter, will not show. + isMember: false, + proposalCount: 0, + }, + }, + }) + + // Clear saved form data. + setNewDaoAtom(makeDefaultNewDao(chainId)) + + // Navigate to DAO page (underneath the creation modal). + goToDao(coreAddress) + } catch (err) { + // toast.promise above will handle displaying the error + console.error(err) + setCreating(false) + } + // Don't stop creating on success, since we are navigating to a new + // page and want to prevent creating duplicate DAOs. + } else { + toast.error(t('error.logInToCreate')) } - // Save values to state. - setNewDaoAtom((prevNewDao) => ({ - ...prevNewDao, - // Deep clone to prevent values from becoming readOnly. - ...cloneDeep(values), - })) - - // Clear custom validation function in case next page does not override - // the previous page's. - setCustomValidator(undefined) - - // Navigate pages. - const pageDelta = parseSubmitterValueDelta(submitterValue) - setPageIndex( - Math.min(Math.max(0, pageIndex + pageDelta), CreateDaoPages.length - 1) - ) - }, - [ - form, - setNewDaoAtom, - parseSubmitterValueDelta, - pageIndex, - isWalletConnected, - createDaoWithFactory, - t, - mode, - awaitNextBlock, - refreshBalances, - chainId, - creatorId, - daoVotingTokenBasedCreatorData, - setDaoCreatedCardProps, - name, - description, - imageUrl, - parentDao, - goToDao, - setFollowing, - ] - ) + return + } - const onError: SubmitErrorHandler = useCallback( - (errors, event) => { - const nativeEvent = event?.nativeEvent as SubmitEvent - const submitterValue = (nativeEvent?.submitter as HTMLInputElement)?.value + // Save values to state. + setNewDaoAtom((prevNewDao) => ({ + ...prevNewDao, + // Deep clone to prevent values from becoming readOnly. + ...cloneDeep(values), + })) + + // Clear custom validation function in case next page does not override + // the previous page's. + setCustomValidator(undefined) + + // Navigate pages. + const pageDelta = parseSubmitterValueDelta(submitterValue) + setPageIndex( + Math.min(Math.max(0, pageIndex + pageDelta), CreateDaoPages.length - 1) + ) + } - // Allow backwards navigation without valid fields. - const pageDelta = parseSubmitterValueDelta(submitterValue) - if (pageDelta < 0) { - return onSubmit(form.getValues(), event) - } else { - console.error('Form errors', errors) - } - }, - [form, onSubmit, parseSubmitterValueDelta] - ) + const onError: SubmitErrorHandler = (errors, event) => { + const nativeEvent = event?.nativeEvent as SubmitEvent + const submitterValue = (nativeEvent?.submitter as HTMLInputElement)?.value - const _handleSubmit = useMemo( - () => form.handleSubmit(onSubmit, onError), - [form, onSubmit, onError] - ) + // Allow backwards navigation without valid fields. + const pageDelta = parseSubmitterValueDelta(submitterValue) + if (pageDelta < 0) { + return onSubmit(form.getValues(), event) + } else { + console.error('Form errors', errors) + } + } - const formOnSubmit = useCallback( - (...args: Parameters) => { - const nativeEvent = args[0]?.nativeEvent as SubmitEvent - const submitterValue = (nativeEvent?.submitter as HTMLInputElement)?.value - const pageDelta = parseSubmitterValueDelta(submitterValue) - - // Validate here instead of in onSubmit since custom errors prevent form - // submission, and we still want to be able to move backwards. - customValidator?.( - // Only set new errors when progressing. If going back, don't. - pageDelta > 0 - ) + const _handleSubmit = form.handleSubmit(onSubmit, onError) + const formOnSubmit = (...args: Parameters) => { + const nativeEvent = args[0]?.nativeEvent as SubmitEvent + const submitterValue = (nativeEvent?.submitter as HTMLInputElement)?.value + const pageDelta = parseSubmitterValueDelta(submitterValue) + + // Validate here instead of in onSubmit since custom errors prevent form + // submission, and we still want to be able to move backwards. + customValidator?.( + // Only set new errors when progressing. If going back, don't. + pageDelta > 0 + ) - return _handleSubmit(...args) - }, - [parseSubmitterValueDelta, customValidator, _handleSubmit] - ) + return _handleSubmit(...args) + } - const createDaoContext: CreateDaoContext = useMemo( - () => ({ - form, - generateInstantiateMsg, - setCustomValidator: (fn) => setCustomValidator(() => fn), - commonVotingConfig: loadCommonVotingConfigItems(), - availableCreators, - creator, - proposalModuleDaoCreationAdapters, - SuspenseLoader, - }), - [ - form, - generateInstantiateMsg, - availableCreators, - creator, - proposalModuleDaoCreationAdapters, - ] - ) + const createDaoContext: CreateDaoContext = { + form, + instantiateMsg, + instantiateMsgError, + setCustomValidator: (fn) => setCustomValidator(() => fn), + commonVotingConfig: loadCommonVotingConfigItems(), + availableCreators, + creator, + proposalModuleDaoCreationAdapters, + SuspenseLoader, + } const Page = CreateDaoPages[pageIndex] - const onReviewPage = pageIndex === CreateDaoPages.length - 1 - const instantiateMsgFunds = useMemo( - () => - // Only compute when on review page. - onReviewPage - ? getFundsFromDaoInstantiateMsg(generateInstantiateMsg()) - : null, - [generateInstantiateMsg, onReviewPage] - ) - return ( <> diff --git a/packages/stateful/creators/MembershipBased/instantiate_schema.json b/packages/stateful/creators/MembershipBased/instantiate_schema.json index 9a164b54e..165b9c5a6 100644 --- a/packages/stateful/creators/MembershipBased/instantiate_schema.json +++ b/packages/stateful/creators/MembershipBased/instantiate_schema.json @@ -2,20 +2,59 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "InstantiateMsg", "type": "object", - "required": ["cw4_group_code_id", "initial_members"], + "required": ["group_contract"], "properties": { - "cw4_group_code_id": { - "type": "integer", - "minimum": 0.0 - }, - "initial_members": { - "type": "array", - "items": { - "$ref": "#/definitions/Member" - } + "group_contract": { + "$ref": "#/definitions/GroupContract" } }, + "additionalProperties": false, "definitions": { + "GroupContract": { + "oneOf": [ + { + "type": "object", + "required": ["existing"], + "properties": { + "existing": { + "type": "object", + "required": ["address"], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": ["new"], + "properties": { + "new": { + "type": "object", + "required": ["cw4_group_code_id", "initial_members"], + "properties": { + "cw4_group_code_id": { + "type": "integer", + "minimum": 0.0 + }, + "initial_members": { + "type": "array", + "items": { + "$ref": "#/definitions/Member" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, "Member": { "description": "A group member has a weight associated with them. This may all be equal, or may have meaning in the app that makes use of the group (eg. voting power)", "type": "object", @@ -28,7 +67,8 @@ "type": "integer", "minimum": 0.0 } - } + }, + "additionalProperties": false } } } diff --git a/packages/stateful/creators/MembershipBased/instantiate_schema_2.1.json b/packages/stateful/creators/MembershipBased/instantiate_schema_2.1.json new file mode 100644 index 000000000..9a164b54e --- /dev/null +++ b/packages/stateful/creators/MembershipBased/instantiate_schema_2.1.json @@ -0,0 +1,34 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": ["cw4_group_code_id", "initial_members"], + "properties": { + "cw4_group_code_id": { + "type": "integer", + "minimum": 0.0 + }, + "initial_members": { + "type": "array", + "items": { + "$ref": "#/definitions/Member" + } + } + }, + "definitions": { + "Member": { + "description": "A group member has a weight associated with them. This may all be equal, or may have meaning in the app that makes use of the group (eg. voting power)", + "type": "object", + "required": ["addr", "weight"], + "properties": { + "addr": { + "type": "string" + }, + "weight": { + "type": "integer", + "minimum": 0.0 + } + } + } + } +} diff --git a/packages/stateful/creators/MembershipBased/mutate.ts b/packages/stateful/creators/MembershipBased/mutate.ts index 599adfb82..c1f12ac3d 100644 --- a/packages/stateful/creators/MembershipBased/mutate.ts +++ b/packages/stateful/creators/MembershipBased/mutate.ts @@ -1,16 +1,17 @@ import { Buffer } from 'buffer' -import { DaoCreatorMutate } from '@dao-dao/types' +import { ChainId, DaoCreatorMutate } from '@dao-dao/types' import { InstantiateMsg, Member } from '@dao-dao/types/contracts/DaoVotingCw4' import { MembershipBasedCreatorId } from '@dao-dao/utils' import { makeValidateMsg } from '@dao-dao/utils/validation/makeValidateMsg' import instantiateSchema from './instantiate_schema.json' +import instantiateSchema21 from './instantiate_schema_2.1.json' import { CreatorData } from './types' export const mutate: DaoCreatorMutate = ( msg, - { name }, + { chainId, name }, { tiers }, t, codeIds @@ -22,18 +23,27 @@ export const mutate: DaoCreatorMutate = ( })) ) - const votingModuleAdapterInstantiateMsg: InstantiateMsg = { - group_contract: { - new: { - cw4_group_code_id: codeIds.Cw4Group, - initial_members: initialMembers, - }, - }, + const newData = { + cw4_group_code_id: codeIds.Cw4Group, + initial_members: initialMembers, } + const votingModuleAdapterInstantiateMsg: InstantiateMsg = + // TODO(neutron-2.3.0): remove this once upgraded. + chainId === ChainId.NeutronMainnet + ? (newData as any) + : { + group_contract: { + new: newData, + }, + } + // Validate and throw error if invalid according to JSON schema. makeValidateMsg( - instantiateSchema, + // TODO(neutron-2.3.0): remove this once upgraded. + chainId === ChainId.NeutronMainnet + ? instantiateSchema21 + : instantiateSchema, t )(votingModuleAdapterInstantiateMsg) @@ -45,7 +55,10 @@ export const mutate: DaoCreatorMutate = ( JSON.stringify(votingModuleAdapterInstantiateMsg), 'utf8' ).toString('base64'), - funds: [], + // TODO(neutron-2.3.0): add back in here and in dao-core instantiate schema. + ...(chainId !== ChainId.NeutronMainnet && { + funds: [], + }), } return msg diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/daoCreation/getInstantiateInfo.ts b/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/daoCreation/getInstantiateInfo.ts index d71904bb1..a8961c3bd 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/daoCreation/getInstantiateInfo.ts +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/daoCreation/getInstantiateInfo.ts @@ -1,6 +1,7 @@ import { Buffer } from 'buffer' import { + ChainId, DaoCreationGetInstantiateInfo, PercentOrMajorityValue, } from '@dao-dao/types' @@ -22,6 +23,7 @@ import preProposeInstantiateSchema from './pre_propose_instantiate_schema.json' export const getInstantiateInfo: DaoCreationGetInstantiateInfo = ( codeIds, { + chainId, name, votingConfig: { quorum, @@ -90,7 +92,10 @@ export const getInstantiateInfo: DaoCreationGetInstantiateInfo = ( JSON.stringify(preProposeMultipleInstantiateMsg), 'utf8' ).toString('base64'), - funds: [], + // TODO(neutron-2.3.0): add back in here and in instantiate schema. + ...(chainId !== ChainId.NeutronMainnet && { + funds: [], + }), }, }, }, @@ -109,7 +114,10 @@ export const getInstantiateInfo: DaoCreationGetInstantiateInfo = ( code_id: codeIds.DaoProposalMultiple, label: `DAO_${name}_${DaoProposalMultipleAdapterId}`, msg: Buffer.from(JSON.stringify(msg), 'utf8').toString('base64'), - funds: [], + // TODO(neutron-2.3.0): add back in here and in instantiate schema. + ...(chainId !== ChainId.NeutronMainnet && { + funds: [], + }), } } diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/daoCreation/instantiate_schema.json b/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/daoCreation/instantiate_schema.json index 41f074153..4470f790e 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/daoCreation/instantiate_schema.json +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/daoCreation/instantiate_schema.json @@ -147,7 +147,7 @@ "ModuleInstantiateInfo": { "description": "Information needed to instantiate a module.", "type": "object", - "required": ["code_id", "funds", "label", "msg"], + "required": ["code_id", "label", "msg"], "properties": { "admin": { "description": "CosmWasm level admin of the instantiated contract. See: ", diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/daoCreation/getInstantiateInfo.ts b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/daoCreation/getInstantiateInfo.ts index d28cb26d7..db63e06a6 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/daoCreation/getInstantiateInfo.ts +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/daoCreation/getInstantiateInfo.ts @@ -1,6 +1,7 @@ import { Buffer } from 'buffer' import { + ChainId, DaoCreationGetInstantiateInfo, PercentOrMajorityValue, } from '@dao-dao/types' @@ -23,6 +24,7 @@ export const getInstantiateInfo: DaoCreationGetInstantiateInfo< > = ( codeIds, { + chainId, name, votingConfig: { quorum, @@ -91,7 +93,10 @@ export const getInstantiateInfo: DaoCreationGetInstantiateInfo< JSON.stringify(preProposeSingleInstantiateMsg), 'utf8' ).toString('base64'), - funds: [], + // TODO(neutron-2.3.0): add back in here and in instantiate schema. + ...(chainId !== ChainId.NeutronMainnet && { + funds: [], + }), }, }, }, @@ -112,7 +117,10 @@ export const getInstantiateInfo: DaoCreationGetInstantiateInfo< code_id: codeIds.DaoProposalSingle, label: `DAO_${name}_${DaoProposalSingleAdapterId}`, msg: Buffer.from(JSON.stringify(msg), 'utf8').toString('base64'), - funds: [], + // TODO(neutron-2.3.0): add back in here and in instantiate schema. + ...(chainId !== ChainId.NeutronMainnet && { + funds: [], + }), } } diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/daoCreation/instantiate_schema.json b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/daoCreation/instantiate_schema.json index eb7b01c63..c77969688 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/daoCreation/instantiate_schema.json +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/daoCreation/instantiate_schema.json @@ -147,7 +147,7 @@ "ModuleInstantiateInfo": { "description": "Information needed to instantiate a module.", "type": "object", - "required": ["code_id", "funds", "label", "msg"], + "required": ["code_id", "label", "msg"], "properties": { "admin": { "description": "CosmWasm level admin of the instantiated contract. See: ", diff --git a/packages/stateless/components/VotingPowerDistribution.tsx b/packages/stateless/components/VotingPowerDistribution.tsx index 44a659c5a..d4ad020bb 100644 --- a/packages/stateless/components/VotingPowerDistribution.tsx +++ b/packages/stateless/components/VotingPowerDistribution.tsx @@ -52,7 +52,7 @@ export const VotingPowerDistribution = ({ : votingPowerPercent.data return ( - + {address ? ( ) : ( diff --git a/packages/stateless/components/dao/create/pages/CreateDaoReview.tsx b/packages/stateless/components/dao/create/pages/CreateDaoReview.tsx index 65cb75686..ecb909497 100644 --- a/packages/stateless/components/dao/create/pages/CreateDaoReview.tsx +++ b/packages/stateless/components/dao/create/pages/CreateDaoReview.tsx @@ -1,4 +1,5 @@ -import { useCallback, useEffect, useRef, useState } from 'react' +import cloneDeep from 'lodash.clonedeep' +import { useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { CreateDaoContext } from '@dao-dao/types' @@ -13,7 +14,8 @@ export const CreateDaoReview = ({ commonVotingConfig, creator, proposalModuleDaoCreationAdapters, - generateInstantiateMsg, + instantiateMsg, + instantiateMsgError, }: CreateDaoContext) => { const { t } = useTranslation() @@ -24,91 +26,54 @@ export const CreateDaoReview = ({ votingConfig, } = newDao - const [decodeModuleMessages, setDecodeModuleMessages] = useState(true) const togglePreviewRef = useRef(null) - const [previewJson, setPreviewJson] = useState() - const [previewError, setPreviewError] = useState() - const [scrollToPreview, setScrollToPreview] = useState(false) - const generatePreview = useCallback( - (scroll = true) => { - setPreviewJson(undefined) - setPreviewError(undefined) - - try { - const msg = generateInstantiateMsg() - // Convert encoded module instantiation messages back to readable JSON. - if (decodeModuleMessages) { - msg.proposal_modules_instantiate_info.forEach((info) => { - const msg = parseEncodedMessage(info.msg) - - // Convert encoded pre_propose_info message back to readable JSON. - if ( - 'pre_propose_info' in msg && - 'module_may_propose' in msg.pre_propose_info && - 'info' in msg.pre_propose_info.module_may_propose && - 'msg' in msg.pre_propose_info.module_may_propose.info - ) { - msg.pre_propose_info.module_may_propose.info.msg = - parseEncodedMessage( - msg.pre_propose_info.module_may_propose.info.msg - ) - } + const [decodeModuleMessages, setDecodeModuleMessages] = useState(true) + const [showingPreview, setShowingPreview] = useState(false) + + let previewJson: string | undefined + let previewError: string | undefined + try { + if (instantiateMsgError) { + throw new Error(instantiateMsgError) + } else if (!instantiateMsg) { + throw new Error(t('error.daoCreationIncomplete')) + } - info.msg = msg - }) - msg.voting_module_instantiate_info.msg = parseEncodedMessage( - msg.voting_module_instantiate_info.msg - ) + const msg = cloneDeep(instantiateMsg) + // Convert encoded module instantiation messages back to readable JSON. + if (decodeModuleMessages) { + msg.proposal_modules_instantiate_info.forEach((info) => { + const msg = parseEncodedMessage(info.msg) + + // Convert encoded pre_propose_info message back to readable JSON. + if ( + 'pre_propose_info' in msg && + 'module_may_propose' in msg.pre_propose_info && + 'info' in msg.pre_propose_info.module_may_propose && + 'msg' in msg.pre_propose_info.module_may_propose.info + ) { + msg.pre_propose_info.module_may_propose.info.msg = + parseEncodedMessage( + msg.pre_propose_info.module_may_propose.info.msg + ) } - // Pretty print output. - setPreviewJson(JSON.stringify(msg, undefined, 2)) - } catch (err) { - console.error(err) - setPreviewError(processError(err)) - } finally { - // Scroll to preview output or error once the state updates. - setScrollToPreview(scroll) - } - }, - [decodeModuleMessages, generateInstantiateMsg] - ) - // When scrollToPreview is true and either previewJson or previewError is set, - // scroll to it. This effect waits for the state to update before scrolling, - // since the DOM needs time to render. - useEffect(() => { - if (!scrollToPreview || (!previewJson && !previewError)) { - return - } - // Scroll. - togglePreviewRef.current?.scrollIntoView({ - behavior: 'smooth', + info.msg = msg + }) + msg.voting_module_instantiate_info.msg = parseEncodedMessage( + msg.voting_module_instantiate_info.msg + ) + } + // Pretty print output. + previewJson = JSON.stringify(msg, undefined, 2) + } catch (err) { + console.error(err) + previewError = processError(err, { + forceCapture: false, }) + } - setScrollToPreview(false) - }, [previewJson, previewError, scrollToPreview]) - - const togglePreview = useCallback(() => { - // If already displaying and error does not exist (should always be true - // together), clear. Otherwise generate the preview. This ensures that if an - // error occurred, it will still try again. - if (previewJson && !previewError) { - setPreviewJson(undefined) - setPreviewError(undefined) - } else { - generatePreview() - } - }, [generatePreview, previewError, previewJson]) - - // If a message is showing and the function reference updates, indicating that - // some input (from the dependency array of the useCallback hook) has changed, - // regenerate the preview but don't forcibly scroll. - useEffect(() => { - if (previewJson) { - generatePreview(false) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [generatePreview]) + const togglePreview = () => setShowingPreview((s) => !s) return ( <> @@ -217,14 +182,13 @@ export const CreateDaoReview = ({ ref={togglePreviewRef} >
- - +

{t('button.showInstantiateMessage')}

- {!!previewJson && ( + {showingPreview && (
)}
- {previewJson && ( + + {showingPreview && !!previewJson && (
)} - {previewError && ( + {!!previewError && (

{previewError}

)} diff --git a/packages/types/contracts/DaoCore.v2.instantiate_schema.json b/packages/types/contracts/DaoCore.v2.instantiate_schema.json index 1545b29a6..a560daf15 100644 --- a/packages/types/contracts/DaoCore.v2.instantiate_schema.json +++ b/packages/types/contracts/DaoCore.v2.instantiate_schema.json @@ -24,7 +24,7 @@ "type": "boolean" }, "dao_uri": { - "description": "Implements the DAO Star standard: https://daostar.one/EIP", + "description": "Implements the DAO Star standard: ", "type": ["string", "null"] }, "description": { @@ -36,7 +36,7 @@ "type": ["string", "null"] }, "initial_items": { - "description": "Initial information for arbitrary contract addresses to be added to the items map. The key is the name of the item in the items map. The value is an enum that either uses an existing address or instantiates a new contract.", + "description": "The items to instantiate this DAO with. Items are arbitrary key-value pairs whose contents are controlled by governance.\n\nIt is an error to provide two items with the same key.", "type": ["array", "null"], "items": { "$ref": "#/definitions/InitialItem" @@ -47,7 +47,7 @@ "type": "string" }, "proposal_modules_instantiate_info": { - "description": "Instantiate information for the core contract's proposal modules.", + "description": "Instantiate information for the core contract's proposal modules. NOTE: the pre-propose-base package depends on it being the case that the core module instantiates its proposal module.", "type": "array", "items": { "$ref": "#/definitions/ModuleInstantiateInfo" @@ -62,6 +62,7 @@ ] } }, + "additionalProperties": false, "definitions": { "Admin": { "description": "Information about the CosmWasm level admin of a contract. Used in conjunction with `ModuleInstantiateInfo` to instantiate modules.", @@ -78,7 +79,8 @@ "addr": { "type": "string" } - } + }, + "additionalProperties": false } }, "additionalProperties": false @@ -89,7 +91,8 @@ "required": ["core_module"], "properties": { "core_module": { - "type": "object" + "type": "object", + "additionalProperties": false } }, "additionalProperties": false @@ -100,6 +103,18 @@ "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", "type": "string" }, + "Coin": { + "type": "object", + "required": ["amount", "denom"], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, "InitialItem": { "description": "Information about an item to be stored in the items list.", "type": "object", @@ -113,7 +128,8 @@ "description": "The value the item will have at instantiation time.", "type": "string" } - } + }, + "additionalProperties": false }, "ModuleInstantiateInfo": { "description": "Information needed to instantiate a module.", @@ -136,6 +152,13 @@ "type": "integer", "minimum": 0.0 }, + "funds": { + "description": "Funds to be sent to the instantiated contract.", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, "label": { "description": "Label for the instantiated contract.", "type": "string" @@ -148,7 +171,12 @@ } ] } - } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" } } } diff --git a/packages/types/contracts/common.ts b/packages/types/contracts/common.ts index 54510833a..965cf00cb 100644 --- a/packages/types/contracts/common.ts +++ b/packages/types/contracts/common.ts @@ -261,7 +261,9 @@ export interface ModuleInstantiateInfo { label: string msg: Binary // Added in V2.3 - funds: Coin[] + // TODO(neutron-2.3.0): make not optional once upgraded and add back into + // instantiate schemas. + funds?: Coin[] } export interface ContractVersionInfo { diff --git a/packages/types/dao.ts b/packages/types/dao.ts index fd96f37e0..760f5ca64 100644 --- a/packages/types/dao.ts +++ b/packages/types/dao.ts @@ -100,11 +100,12 @@ export type CreateDaoCustomValidator = (setNewErrors: boolean) => void export interface CreateDaoContext { form: UseFormReturn> + instantiateMsg: DaoCoreV2InstantiateMsg | undefined + instantiateMsgError: string | undefined commonVotingConfig: DaoCreationCommonVotingConfigItems availableCreators: readonly DaoCreator[] creator: DaoCreator proposalModuleDaoCreationAdapters: Required['daoCreation'][] - generateInstantiateMsg: () => DaoCoreV2InstantiateMsg setCustomValidator: (fn: CreateDaoCustomValidator) => void SuspenseLoader: ComponentType } diff --git a/packages/utils/dao.ts b/packages/utils/dao.ts index c6a7d48fc..6c82ccf4d 100644 --- a/packages/utils/dao.ts +++ b/packages/utils/dao.ts @@ -58,6 +58,8 @@ export const getFundsFromDaoInstantiateMsg = ({ voting_module_instantiate_info, proposal_modules_instantiate_info, }: DaoCoreV2InstantiateMsg) => [ - ...voting_module_instantiate_info.funds, - ...proposal_modules_instantiate_info.flatMap(({ funds }) => funds), + // TODO(neutron-2.3.0): remove once non-optional + ...(voting_module_instantiate_info.funds || []), + // TODO(neutron-2.3.0): remove once non-optional + ...proposal_modules_instantiate_info.flatMap(({ funds }) => funds || []), ] diff --git a/packages/utils/validation/makeValidateMsg.ts b/packages/utils/validation/makeValidateMsg.ts index b41571ca9..0cc62b32c 100644 --- a/packages/utils/validation/makeValidateMsg.ts +++ b/packages/utils/validation/makeValidateMsg.ts @@ -8,7 +8,7 @@ export class AjvInvalidMessageError extends Error { errors: ErrorObject, unknown>[], t?: TFunction ) { - console.log(errors) + console.error('AJV:', errors) super( (t?.('error.invalidMessage') ?? 'Invalid message') + ': ' +