diff --git a/apps/dapp/pages/dao/[address]/[[...slug]].tsx b/apps/dapp/pages/dao/[address]/[[...slug]].tsx index 5025580ba..79bccb6ec 100644 --- a/apps/dapp/pages/dao/[address]/[[...slug]].tsx +++ b/apps/dapp/pages/dao/[address]/[[...slug]].tsx @@ -27,7 +27,7 @@ import { useDaoInfoContext, useDaoNavHelpers, } from '@dao-dao/stateless' -import { ActionKey, DaoPageMode } from '@dao-dao/types' +import { ActionKey, DaoPageMode, DaoTabId } from '@dao-dao/types' import { SITE_URL, getDaoPath, @@ -150,12 +150,16 @@ const InnerDaoHome = () => { useFollowingDaos(daoInfo.chainId) const following = isFollowing(daoInfo.coreAddress) - const tabs = useDaoTabs() - const firstTabId = tabs[0].id + const loadingTabs = useDaoTabs() + // Just a type-check because some tabs are loaded at the beginning. + const tabs = loadingTabs.loading ? undefined : loadingTabs.data + // Default to proposals tab for type-check, but should never happen as some + // tabs are loaded at the beginning. + const firstTabId = tabs?.[0].id || DaoTabId.Proposals // Pre-fetch tabs. useEffect(() => { - tabs.forEach((tab) => { + tabs?.forEach((tab) => { router.prefetch(getDaoPath(daoInfo.coreAddress, tab.id)) }) }, [daoInfo.coreAddress, getDaoPath, router, tabs]) @@ -171,7 +175,7 @@ const InnerDaoHome = () => { }, [daoInfo.coreAddress, getDaoPath, router, slug.length, firstTabId]) const tabId = - slug.length > 0 && tabs.some(({ id }) => id === slug[0]) + slug.length > 0 && tabs?.some(({ id }) => id === slug[0]) ? slug[0] : // If tab is invalid, default to first tab. firstTabId @@ -197,7 +201,7 @@ const InnerDaoHome = () => { onSelectTabId={onSelectTabId} rightSidebarContent={} selectedTabId={tabId} - tabs={tabs} + tabs={tabs || []} /> ) } diff --git a/apps/sda/pages/[address]/[[...slug]].tsx b/apps/sda/pages/[address]/[[...slug]].tsx index bebdee093..08f668043 100644 --- a/apps/sda/pages/[address]/[[...slug]].tsx +++ b/apps/sda/pages/[address]/[[...slug]].tsx @@ -16,7 +16,7 @@ import { useDaoInfoContext, useDaoNavHelpers, } from '@dao-dao/stateless' -import { DaoPageMode } from '@dao-dao/types' +import { DaoPageMode, DaoTabId } from '@dao-dao/types' import { SITE_URL, getDaoPath } from '@dao-dao/utils' const DaoHomePage: NextPage = () => { @@ -24,12 +24,16 @@ const DaoHomePage: NextPage = () => { const { coreAddress } = useDaoInfoContext() const { getDaoPath } = useDaoNavHelpers() - const tabs = useDaoTabs() - const firstTabId = tabs[0].id + const loadingTabs = useDaoTabs() + // Just a type-check because some tabs are loaded at the beginning. + const tabs = loadingTabs.loading ? undefined : loadingTabs.data + // Default to proposals tab for type-check, but should never happen as some + // tabs are loaded at the beginning. + const firstTabId = tabs?.[0].id || DaoTabId.Proposals // Pre-fetch tabs. useEffect(() => { - tabs.forEach((tab) => { + tabs?.forEach((tab) => { router.prefetch(getDaoPath(coreAddress, tab.id)) }) }, [coreAddress, getDaoPath, router, tabs]) @@ -45,7 +49,7 @@ const DaoHomePage: NextPage = () => { }, [coreAddress, getDaoPath, router, slug.length, firstTabId]) const selectedTabId = - slug.length > 0 && tabs.some(({ id }) => id === slug[0]) + slug.length > 0 && tabs?.some(({ id }) => id === slug[0]) ? slug[0] : // If tab is invalid, default to first tab. firstTabId @@ -53,7 +57,7 @@ const DaoHomePage: NextPage = () => { return ( } tabId={selectedTabId} /> diff --git a/packages/i18n/locales/en/translation.json b/packages/i18n/locales/en/translation.json index 4d5b043ca..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.", @@ -605,6 +606,7 @@ "tokenAddress": "Token address", "tokenContractAddressTitle": "Token contract address", "tokenDefinition": "Token definition", + "tokenIdentifier": "Token identifier", "tokenInfo": "Token info", "tokenInstructions": "Token instructions", "tokenSwapCreateInstructions": "In this step, you will create the token swap. This creation step describes how many funds each party needs to send for the swap to complete. <2>No funds will be transferred at this time.", @@ -1438,7 +1440,6 @@ "timeRemaining": "Time remaining", "title": "Title", "token": "Token", - "tokenIdentifier": "Token identifier", "tokenSwap": "Token Swap", "topAbsolute": "Top {{count}}", "topPercent": "Top {{percent}}%", diff --git a/packages/stateful/command/contexts/generic/dao.tsx b/packages/stateful/command/contexts/generic/dao.tsx index 19cb63b0e..2b31bafcf 100644 --- a/packages/stateful/command/contexts/generic/dao.tsx +++ b/packages/stateful/command/contexts/generic/dao.tsx @@ -39,7 +39,9 @@ export const makeGenericDaoContext: CommandModalContextMaker<{ const { t } = useTranslation() const { getDaoPath, getDaoProposalPath, router } = useDaoNavHelpers() const { coreVersion } = useDaoInfoContext() - const tabs = useDaoTabs() + const loadingTabs = useDaoTabs({ + suspendWhileLoadingOverride: false, + }) const { isFollowing, setFollowing, setUnfollowing, updatingFollowing } = useFollowingDaos(chainId) @@ -72,7 +74,9 @@ export const makeGenericDaoContext: CommandModalContextMaker<{ const routes = [ daoPageHref, createProposalHref, - ...tabs.map(({ id }) => getDaoPath(coreAddress, id)), + ...(loadingTabs.loading + ? [] + : loadingTabs.data.map(({ id }) => getDaoPath(coreAddress, id))), ] useDeepCompareEffect(() => { routes.forEach((url) => router.prefetch(url)) @@ -153,16 +157,19 @@ export const makeGenericDaoContext: CommandModalContextMaker<{ setNavigatingToHref(href) } }, - items: tabs.map(({ id, label, Icon }) => { - const href = getDaoPath(coreAddress, id) - - return { - name: label, - Icon, - href, - loading: navigatingToHref === href, + loading: loadingTabs.loading || loadingTabs.updating, + items: (loadingTabs.loading ? [] : loadingTabs.data).map( + ({ id, label, Icon }) => { + const href = getDaoPath(coreAddress, id) + + return { + name: label, + Icon, + href, + loading: navigatingToHref === href, + } } - }), + ), } const subDaosSection: CommandModalContextSection = { @@ -174,7 +181,7 @@ export const makeGenericDaoContext: CommandModalContextMaker<{ dao, }) ), - loading: subDaosLoading.loading, + loading: subDaosLoading.loading || subDaosLoading.updating, items: subDaosLoading.loading ? [] : subDaosLoading.data.map( diff --git a/packages/stateful/components/SdaLayout.tsx b/packages/stateful/components/SdaLayout.tsx index 5e24c5d27..e94e1da7b 100644 --- a/packages/stateful/components/SdaLayout.tsx +++ b/packages/stateful/components/SdaLayout.tsx @@ -54,7 +54,7 @@ export const SdaLayout = ({ children }: { children: ReactNode }) => { daoCreatedCardPropsAtom ) - const tabs = useDaoTabs() + const loadingTabs = useDaoTabs() return ( { connectWalletButton={} connected={isWalletConnected} navigationProps={{ - tabs, + tabs: loadingTabs.loading ? [] : loadingTabs.data, LinkWrapper, version: '2.0', compact, 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/hooks/useDaoTabs.ts b/packages/stateful/hooks/useDaoTabs.ts index 7295fc179..888bba9f4 100644 --- a/packages/stateful/hooks/useDaoTabs.ts +++ b/packages/stateful/hooks/useDaoTabs.ts @@ -6,6 +6,7 @@ import { QuestionMark, WebOutlined, } from '@mui/icons-material' +import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useAppContext } from '@dao-dao/stateless' @@ -13,6 +14,7 @@ import { DaoPageMode, DaoTabId, DaoTabWithComponent, + LoadingData, WidgetLocation, } from '@dao-dao/types' @@ -27,7 +29,15 @@ import { import { useVotingModuleAdapter } from '../voting-module-adapter' import { useWidgets } from '../widgets' -export const useDaoTabs = (): DaoTabWithComponent[] => { +export type UseDaoTabsOptions = { + // If defined, will override the default suspendWhileLoading behavior of the + // DAO home widgets. + suspendWhileLoadingOverride?: boolean +} + +export const useDaoTabs = ({ + suspendWhileLoadingOverride, +}: UseDaoTabsOptions = {}): LoadingData => { const { t } = useTranslation() const { mode } = useAppContext() @@ -40,28 +50,14 @@ export const useDaoTabs = (): DaoTabWithComponent[] => { // Only load tab widgets. location: WidgetLocation.Tab, }) - const widgetTabs = loadingWidgets.loading - ? [] - : loadingWidgets.data.map( - ({ - title, - widget: { id, Icon }, - WidgetComponent, - }): DaoTabWithComponent => ({ - id, - label: title, - // Icon should always be defined for tab widgets, but just in case... - Icon: Icon || QuestionMark, - Component: WidgetComponent, - }) - ) // Add home tab with widgets if any widgets exist. const loadingDaoHomeWidgets = useWidgets({ // In dApp, load widgets before rendering to decide if home with widgets is // shown so that we know to select home by default when present. In SDA, no // need to load widgets before rendering since the home is always shown. - suspendWhileLoading: mode === DaoPageMode.Dapp, + suspendWhileLoading: + suspendWhileLoadingOverride ?? mode === DaoPageMode.Dapp, // Only load home widgets. location: WidgetLocation.Home, }) @@ -75,45 +71,79 @@ export const useDaoTabs = (): DaoTabWithComponent[] => { ? DaoWidgets : undefined - return [ - ...(HomeTab - ? [ - { - id: DaoTabId.Home, - label: t('title.home'), - Component: HomeTab, - Icon: HomeOutlined, - }, - ] - : []), - { - id: DaoTabId.Proposals, - label: t('title.proposals'), - Component: ProposalsTab, - Icon: HowToVoteOutlined, - }, - { - id: DaoTabId.Treasury, - label: t('title.treasuryAndNfts'), - Component: TreasuryAndNftsTab, - Icon: AccountBalanceWalletOutlined, - }, - { - id: DaoTabId.SubDaos, - label: t('title.subDaos'), - Component: SubDaosTab, - Icon: FiberSmartRecordOutlined, - }, - { - id: DaoTabId.Apps, - label: t('title.apps'), - Component: AppsTab, - Icon: WebOutlined, - }, - ...(extraTabs?.map(({ labelI18nKey, ...tab }) => ({ - label: t(labelI18nKey), - ...tab, - })) ?? []), - ...widgetTabs, - ] + const tabs = useMemo( + () => [ + ...(HomeTab + ? [ + { + id: DaoTabId.Home, + label: t('title.home'), + Component: HomeTab, + Icon: HomeOutlined, + }, + ] + : []), + { + id: DaoTabId.Proposals, + label: t('title.proposals'), + Component: ProposalsTab, + Icon: HowToVoteOutlined, + }, + { + id: DaoTabId.Treasury, + label: t('title.treasuryAndNfts'), + Component: TreasuryAndNftsTab, + Icon: AccountBalanceWalletOutlined, + }, + { + id: DaoTabId.SubDaos, + label: t('title.subDaos'), + Component: SubDaosTab, + Icon: FiberSmartRecordOutlined, + }, + { + id: DaoTabId.Apps, + label: t('title.apps'), + Component: AppsTab, + Icon: WebOutlined, + }, + ...(extraTabs?.map(({ labelI18nKey, ...tab }) => ({ + label: t(labelI18nKey), + ...tab, + })) ?? []), + ...(loadingWidgets.loading + ? [] + : loadingWidgets.data.map( + ({ + title, + widget: { id, Icon }, + WidgetComponent, + }): DaoTabWithComponent => ({ + id, + label: title, + // Icon should always be defined for tab widgets, but just in case... + Icon: Icon || QuestionMark, + Component: WidgetComponent, + }) + )), + ], + [HomeTab, extraTabs, t, loadingWidgets] + ) + + const updating = + loadingWidgets.loading || + loadingWidgets.updating || + loadingDaoHomeWidgets.loading || + loadingDaoHomeWidgets.updating + + return useMemo( + () => ({ + // Some tabs are ready right away, so just use the `updating` field to + // indicate if more tabs are still loading. + loading: false, + updating, + data: tabs, + }), + [updating, tabs] + ) } 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/stateful/recoil/selectors/dao/misc.ts b/packages/stateful/recoil/selectors/dao/misc.ts index 984ac3be9..9f7bcbe9b 100644 --- a/packages/stateful/recoil/selectors/dao/misc.ts +++ b/packages/stateful/recoil/selectors/dao/misc.ts @@ -1,4 +1,9 @@ -import { RecoilValueReadOnly, selectorFamily, waitForAllSettled } from 'recoil' +import { + RecoilValueReadOnly, + selectorFamily, + waitForAll, + waitForAllSettled, +} from 'recoil' import { DaoCoreV2Selectors, @@ -164,38 +169,68 @@ export const daoInfoSelector: (param: { throw new Error('DAO failed to dump state.') } - const coreVersion = get( - contractVersionSelector({ - contractAddress: coreAddress, - chainId, - }) + const [ + // Non-loadables + [ + coreVersion, + votingModuleInfo, + proposalModules, + created, + _items, + polytoneProxies, + ], + // Loadables + [isActiveResponse, activeThresholdResponse], + ] = get( + waitForAll([ + // Non-loadables + waitForAll([ + contractVersionSelector({ + contractAddress: coreAddress, + chainId, + }), + contractInfoSelector({ + contractAddress: dumpState.voting_module, + chainId, + }), + daoCoreProposalModulesSelector({ + coreAddress, + chainId, + }), + contractInstantiateTimeSelector({ + address: coreAddress, + chainId, + }), + DaoCoreV2Selectors.listAllItemsSelector({ + contractAddress: coreAddress, + chainId, + }), + DaoCoreV2Selectors.polytoneProxiesSelector({ + contractAddress: coreAddress, + chainId, + }), + ]), + // Loadables + waitForAllSettled([ + // All voting modules use the same active threshold queries, so it's + // safe to use the cw20-staked selector. + DaoVotingCw20StakedSelectors.isActiveSelector({ + contractAddress: dumpState.voting_module, + chainId, + params: [], + }), + DaoVotingCw20StakedSelectors.activeThresholdSelector({ + contractAddress: dumpState.voting_module, + chainId, + params: [], + }), + ]), + ]) ) - const votingModuleInfo = get( - contractInfoSelector({ - contractAddress: dumpState.voting_module, - chainId, - }) - ) const votingModuleContractName = votingModuleInfo?.info.contract || 'fallback' - // All voting modules use the same isActive query, so it's safe to just - // use one here. - const [isActiveResponse, activeThresholdResponse] = get( - waitForAllSettled([ - DaoVotingCw20StakedSelectors.isActiveSelector({ - contractAddress: dumpState.voting_module, - chainId, - params: [], - }), - DaoVotingCw20StakedSelectors.activeThresholdSelector({ - contractAddress: dumpState.voting_module, - chainId, - params: [], - }), - ]) - ) // Some voting modules don't support the isActive query, so if the query // fails, assume active. const isActive = @@ -207,26 +242,6 @@ export const daoInfoSelector: (param: { activeThresholdResponse.contents.active_threshold) || null - const proposalModules = get( - daoCoreProposalModulesSelector({ - coreAddress, - chainId, - }) - ) - - const created = get( - contractInstantiateTimeSelector({ - address: coreAddress, - chainId, - }) - ) - - const _items = get( - DaoCoreV2Selectors.listAllItemsSelector({ - contractAddress: coreAddress, - chainId, - }) - ) // Convert items list into map. const items = _items.reduce( (acc, [key, value]) => ({ @@ -236,13 +251,6 @@ export const daoInfoSelector: (param: { {} as Record ) - const polytoneProxies = get( - DaoCoreV2Selectors.polytoneProxiesSelector({ - contractAddress: coreAddress, - chainId, - }) - ) - const { admin } = dumpState let parentDaoInfo diff --git a/packages/stateful/widgets/react/useWidgets.tsx b/packages/stateful/widgets/react/useWidgets.tsx index 50fd2dddd..c975e42cc 100644 --- a/packages/stateful/widgets/react/useWidgets.tsx +++ b/packages/stateful/widgets/react/useWidgets.tsx @@ -68,9 +68,9 @@ export const useWidgets = ({ const widgetItemsLoadable = useCachedLoadable(widgetItemsSelector) - const loadedWidgets = useMemo((): LoadedWidget[] | undefined => { + const loadingWidgets = useMemo((): LoadingData => { if (widgetItemsLoadable.state !== 'hasValue') { - return + return { loading: true } } const parsedWidgets = widgetItemsLoadable.contents @@ -89,8 +89,9 @@ export const useWidgets = ({ // Validate widget structure. .filter((widget): widget is DaoWidget => !!widget) - return ( - parsedWidgets + return { + loading: false, + data: parsedWidgets .map((daoWidget): LoadedWidget | undefined => { const widget = getWidgetById(chainId, daoWidget.id) // Enforce location filter. @@ -125,16 +126,9 @@ export const useWidgets = ({ } }) // Filter out any undefined widgets. - .filter((widget): widget is LoadedWidget => !!widget) - ) + .filter((widget): widget is LoadedWidget => !!widget), + } }, [widgetItemsLoadable, isMember, t, location, chainId]) - return loadedWidgets - ? { - loading: false, - data: loadedWidgets, - } - : { - loading: true, - } + return loadingWidgets } 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') + ': ' +