diff --git a/packages/i18n/locales/en/translation.json b/packages/i18n/locales/en/translation.json index 009e800be..5732d09ea 100644 --- a/packages/i18n/locales/en/translation.json +++ b/packages/i18n/locales/en/translation.json @@ -479,6 +479,7 @@ "missingIbcChainAccounts_one": "An account needs to be created on the following chain so it can be used during this multi-hop IBC transaction: {{chains}}. In the event of a catastrophic failure, there is a small chance that a multi-hop IBC transaction will not be reversible, and tokens will end up on one of the intermediary chains. In this case, an account must exist on each intermediary to act as a failsafe. Either a cross-chain or ICA account works and will be automatically detected once created.", "missingIbcChainAccounts_other": "Accounts need to be created on the following chains so they can be used during this multi-hop IBC transaction: {{chains}}. In the event of a catastrophic failure, there is a small chance that a multi-hop IBC transaction will not be reversible, and tokens will end up on one of the intermediary chains. In this case, an account must exist on each intermediary to act as a failsafe. Either a cross-chain or ICA account works and will be automatically detected once created.", "missingNativeToken": "Missing native token.", + "multipleChoiceApprovalNotYetSupported": "Approval does not yet support multiple choice proposals. Disable the multiple choice proposal module before enabling an approver, or wait for multiple choice approval support.", "mustBeAtLeastSixtySeconds": "Must be at least 60 seconds.", "mustBeMemberToAddMember": "You must be a member of the DAO to propose adding a new member.", "mustBeMemberToCreateCompensationCycle": "You must be a member of the DAO to create a compensation cycle.", @@ -966,8 +967,8 @@ "approvalProposalExplanation_pending": "This proposal is waiting to be accepted by the approver so that it can open for voting.", "approvalProposalExplanation_rejected": "This proposal was not allowed to open for voting because the approver rejected it.", "approverEnabledNoMultipleChoice": "Approver is enabled, which is currently incompatible with multiple choice proposals.", - "approverExplanation_choosing": "DAOs shown below have this DAO ({{daoName}}) set as their approver. This action completes the setup process and automates the creation of approval proposals in {{daoName}} when the selected DAO below attempts to create a new proposal. Proposals created in the selected DAO before this setup is complete must be manually approved with the Execute Smart Contract action.", - "approverExplanation_chose": "The DAO shown below has this DAO ({{daoName}}) set as their approver. This action completes the setup process and automates the creation of approval proposals in {{daoName}} when the selected DAO below attempts to create a new proposal. Proposals created in the selected DAO before this setup is complete must be manually approved with the Execute Smart Contract action.", + "approverExplanation_choosing": "DAOs shown below have this DAO ({{daoName}}) set as their approver. This action completes the setup process and automates the creation of approval proposals in {{daoName}} when the selected DAO below attempts to create a new proposal.", + "approverExplanation_chose": "The DAO shown below has this DAO ({{daoName}}) set as their approver. This action completes the setup process and automates the creation of approval proposals in {{daoName}} when the selected DAO below attempts to create a new proposal.", "approverProposalExplanation_closed": "This proposal is responsible for approving a proposal in another DAO. Since it was rejected and closed, the original proposal was not allowed to open for voting.", "approverProposalExplanation_executed": "This proposal is responsible for approving a proposal in another DAO. Since it was passed and executed, the original proposal opened for voting.", "approverProposalExplanation_execution_failed": "This proposal is responsible for approving a proposal in another DAO. An unexpected error occurred (execution failed). Something is wrong.", @@ -983,7 +984,7 @@ "availableBalance": "Available balance", "availableForWithdrawal": "available for withdrawal", "availableHistoryLoaded": "All available history has been loaded.", - "becomeApproverDescription": "Assume responsibility for approving another DAO's proposals before they open for voting.", + "becomeApproverDescription": "Assume responsibility for approving another DAO's proposals before they open for voting. This can only be used after the other DAO sets this DAO as their approver.", "becomeSubDaoActionDescription": "Provide the address of a DAO to request to become its SubDAO. The DAO will then need to accept your request to complete the ownership transfer and become your parent. Once the transfer is complete, only your new parent DAO can transfer ownership in the future. Keep in mind that your parent DAO will have full authority to execute anything it wants to on behalf of this DAO.", "becomeSubDaoActionDescription_created": "The parent DAO needs to accept this request to complete the ownership transfer and become your parent. Once the transfer is complete, only your new parent DAO can transfer ownership in the future. Keep in mind that your parent DAO will have full authority to execute anything it wants to on behalf of this DAO.", "becomeSubDaoDescription": "Request to become a SubDAO of a DAO.", @@ -1089,6 +1090,8 @@ "emailVerificationResent": "Email verification resent.", "emailVerified": "Email verified.", "emptyInboxCaughtUp": "You're all caught up!", + "enableApproverDescription": "Require approval before proposals can open for voting.", + "enableApproverExplanation": "Enable an approver that must approve new proposals in this DAO before they open for voting. If the approver is a DAO, the approver DAO must pass and execute a proposal with the \"Become Approver\" action after this enable action is executed.", "enableMultipleChoiceProposalsDescription": "Allow members to vote on many options in one proposal.", "enableMultipleChoiceProposalsExplanation": "This action will enable the creation of multiple choice proposals, which allow voting on many options in one proposal. Each option can have a list of actions that execute if that option wins. It will be configured to match the current config of the single choice proposal module.", "enableVetoerDaoDescription": "Display proposals from a DAO on the home page when they are vetoable.", @@ -1766,6 +1769,7 @@ "documentation": "Documentation", "editValidator": "Edit validator", "email": "Email", + "enableApprover": "Enable Approver", "enableMultipleChoiceProposals": "Enable Multiple Choice Proposals", "enableRetroactiveCompensation": "Enable Retroactive Compensation", "enableVestingPayments": "Enable Vesting Payments", diff --git a/packages/state/query/queries/proposal.ts b/packages/state/query/queries/proposal.ts index 8d88cdb29..ea1679e0f 100644 --- a/packages/state/query/queries/proposal.ts +++ b/packages/state/query/queries/proposal.ts @@ -9,6 +9,8 @@ import { PreProposeSubmissionPolicy } from '@dao-dao/types/contracts/DaoPrePropo import { Config as NeutronCwdSubdaoTimelockSingleConfig } from '@dao-dao/types/contracts/NeutronCwdSubdaoTimelockSingle' import { ContractName, + DAO_PRE_PROPOSE_MULTIPLE_CONTRACT_NAMES, + DAO_PRE_PROPOSE_SINGLE_CONTRACT_NAMES, getCosmWasmClientForChainId, parseContractVersion, } from '@dao-dao/utils' @@ -45,7 +47,14 @@ export const fetchPreProposeModule = async ( const contractVersion = parseContractVersion(contractInfo.version) let typedConfig: PreProposeModuleTypedConfig = { - type: PreProposeModuleType.Other, + // If normal DAO DAO pre-propose module, use normal. Otherwise, default to + // other. + type: [ + ...DAO_PRE_PROPOSE_SINGLE_CONTRACT_NAMES, + ...DAO_PRE_PROPOSE_MULTIPLE_CONTRACT_NAMES, + ].includes(contractInfo.contract) + ? PreProposeModuleType.Normal + : PreProposeModuleType.Other, } // All pre-propose modules share the same config. @@ -61,7 +70,8 @@ export const fetchPreProposeModule = async ( .catch(() => undefined) switch (contractInfo.contract) { - case ContractName.PreProposeApprovalSingle: { + case ContractName.PreProposeApprovalSingle: + case ContractName.PreProposeApprovalMultiple: { let approver: string | undefined let preProposeApproverContract: string | null = null diff --git a/packages/stateful/actions/core/actions/EnableApprover/Component.tsx b/packages/stateful/actions/core/actions/EnableApprover/Component.tsx new file mode 100644 index 000000000..15a1517e0 --- /dev/null +++ b/packages/stateful/actions/core/actions/EnableApprover/Component.tsx @@ -0,0 +1,67 @@ +import { ComponentType } from 'react' +import { useFormContext } from 'react-hook-form' +import { useTranslation } from 'react-i18next' + +import { + InputErrorMessage, + InputLabel, + useActionOptions, +} from '@dao-dao/stateless' +import { AddressInputProps } from '@dao-dao/types' +import { ActionComponent, ActionContextType } from '@dao-dao/types/actions' +import { makeValidateAddress, validateRequired } from '@dao-dao/utils' + +import { + MultipleChoiceProposalModule, + SecretMultipleChoiceProposalModule, +} from '../../../../clients' + +export type EnableApproverData = { + approver: string +} + +export type EnableApproverOptions = { + AddressInput: ComponentType> +} + +export const EnableApproverComponent: ActionComponent< + EnableApproverOptions +> = ({ isCreating, fieldNamePrefix, errors, options: { AddressInput } }) => { + const { t } = useTranslation() + const { + context, + chain: { bech32_prefix: bech32Prefix }, + } = useActionOptions() + const { register } = useFormContext() + + return ( + <> + {context.type === ActionContextType.Dao && + context.dao.proposalModules.some( + (m) => + m instanceof MultipleChoiceProposalModule || + m instanceof SecretMultipleChoiceProposalModule + ) && ( +

+ {t('error.multipleChoiceApprovalNotYetSupported')} +

+ )} + +

+ {t('info.enableApproverExplanation')} +

+ +
+ + + +
+ + ) +} diff --git a/packages/stateful/actions/core/actions/EnableApprover/README.md b/packages/stateful/actions/core/actions/EnableApprover/README.md new file mode 100644 index 000000000..1085b02cb --- /dev/null +++ b/packages/stateful/actions/core/actions/EnableApprover/README.md @@ -0,0 +1,20 @@ +# EnableApprover + +Enable an approver for an existing proposal module. + +## Bulk import format + +This is relevant when bulk importing actions, as described in [this +guide](https://github.com/DA0-DA0/dao-dao-ui/wiki/Bulk-importing-actions). + +### Key + +`enableApprover` + +### Data format + +```json +{ + "approver": "
" +} +``` diff --git a/packages/stateful/actions/core/actions/EnableApprover/index.tsx b/packages/stateful/actions/core/actions/EnableApprover/index.tsx new file mode 100644 index 000000000..a5493c0ee --- /dev/null +++ b/packages/stateful/actions/core/actions/EnableApprover/index.tsx @@ -0,0 +1,368 @@ +import { contractQueries } from '@dao-dao/state/query' +import { ActionBase, PersonRaisingHandEmoji } from '@dao-dao/stateless' +import { + Feature, + IProposalModuleBase, + ModuleInstantiateInfo, + PreProposeModuleType, + SecretModuleInstantiateInfo, + UnifiedCosmosMsg, +} from '@dao-dao/types' +import { + ActionChainContextType, + ActionComponent, + ActionContextType, + ActionKey, + ActionMatch, + ActionOptions, + ProcessedMessage, +} from '@dao-dao/types/actions' +import { PreProposeInfo } from '@dao-dao/types/contracts/DaoDaoCore' +import { InstantiateMsg as DaoPreProposeApprovalSingleInstantiateMsg } from '@dao-dao/types/contracts/DaoPreProposeApprovalSingle' +import { InstantiateMsg as SecretDaoPreProposeApprovalSingleInstantiateMsg } from '@dao-dao/types/contracts/SecretDaoPreProposeApprovalSingle' +import { + decodeJsonFromBase64, + encodeJsonToBase64, + isFeatureSupportedByVersion, + makeExecuteSmartContractMessage, + objectMatchesStructure, +} from '@dao-dao/utils' + +import { + MultipleChoiceProposalModule, + SecretMultipleChoiceProposalModule, + SecretSingleChoiceProposalModule, + SingleChoiceProposalModule, +} from '../../../../clients' +import { AddressInput } from '../../../../components' +import { EnableApproverComponent, EnableApproverData } from './Component' + +const Component: ActionComponent = (props) => ( + +) + +export class EnableApproverAction extends ActionBase { + public readonly key = ActionKey.EnableApprover + public readonly Component = Component + + private validProposalModules: IProposalModuleBase[] + + protected _defaults = { + approver: '', + } + + constructor(options: ActionOptions) { + // Disallow usage if: + // - not a DAO + // - DAO is not on a supported version + // + // Disallows creation via `hideWithPicker` (at the bottom) if: + // - approver is already enabled + if ( + options.context.type !== ActionContextType.Dao || + !options.context.dao.info.supportedFeatures[Feature.Approval] + ) { + throw new Error('Invalid context for enabling approver') + } + + super(options, { + Icon: PersonRaisingHandEmoji, + label: options.t('title.enableApprover'), + description: options.t('info.enableApproverDescription'), + notReusable: true, + }) + + // Can only add approver to proposal modules that are on a supported version + // and have no pre-propose module or use the normal pre-propose modules. + this.validProposalModules = options.context.dao.proposalModules.filter( + (m) => + isFeatureSupportedByVersion(Feature.Approval, m.version) && + (!m.prePropose || m.prePropose.type === PreProposeModuleType.Normal) + ) + + // Disallow creation if no proposal modules can have approval added. + this.metadata.hideFromPicker = this.validProposalModules.length === 0 + } + + async encode({ approver }: EnableApproverData): Promise { + // Type-check. This is already checked in the constructor. + if ( + this.options.context.type !== ActionContextType.Dao || + this.options.chainContext.type !== ActionChainContextType.Supported + ) { + throw new Error('Invalid context for enabling approver') + } + + // Error if any are multiple choice since approval does not yet support + // multiple choice proposals. + if ( + this.options.context.dao.proposalModules.some( + (m) => + m instanceof MultipleChoiceProposalModule || + m instanceof SecretMultipleChoiceProposalModule + ) + ) { + throw new Error( + this.options.t('error.multipleChoiceApprovalNotYetSupported') + ) + } + + const { allCodeIds, allCodeHashes } = this.options.chainContext.config + + return await Promise.all( + this.validProposalModules.map(async (m) => { + const isSecretSingle = m instanceof SecretSingleChoiceProposalModule + const isSecretMultiple = m instanceof SecretMultipleChoiceProposalModule + const isSecret = isSecretSingle || isSecretMultiple + + const isSingle = + m instanceof SingleChoiceProposalModule || + m instanceof SecretSingleChoiceProposalModule + + let codeId + let codeHash + + if (isSingle) { + codeId = allCodeIds[m.version]?.DaoPreProposeApprovalSingle + codeHash = allCodeHashes?.[m.version]?.DaoPreProposeApprovalSingle + } else { + throw new Error( + this.options.t('error.multipleChoiceApprovalNotYetSupported') + ) + + // TODO(approver-multiple): not yet ready + // codeId = allCodeIds[m.version]?.DaoPreProposeApprovalMultiple + // codeHash = allCodeHashes[m.version]?.DaoPreProposeApprovalMultiple + } + + if (!codeId) { + throw new Error( + 'Pre-propose approval module code ID not found for this version' + ) + } + + if (isSecret && !codeHash) { + throw new Error( + 'Pre-propose approval module code hash not found for this version' + ) + } + + const depositInfo = await this.options.queryClient.fetchQuery( + m.getDepositInfoQuery() + ) + + let msg + if (isSecret) { + const secretDenomCodeHash = + depositInfo && 'cw20' in depositInfo.denom + ? await this.options.queryClient.fetchQuery( + contractQueries.secretCodeHash({ + chainId: this.options.chain.chain_id, + address: depositInfo.denom.cw20, + }) + ) + : '' + + const _msg: SecretDaoPreProposeApprovalSingleInstantiateMsg = { + deposit_info: depositInfo && { + amount: depositInfo.amount, + denom: { + token: { + // transform standard deposit info shape into secret network + // snip20 shape if necessary + denom: + 'cw20' in depositInfo.denom + ? { + snip20: [depositInfo.denom.cw20, secretDenomCodeHash], + } + : depositInfo.denom, + }, + }, + refund_policy: depositInfo.refund_policy, + }, + extension: { + approver, + }, + // If pre-propose module already exists, check if open to anyone. + // Otherwise, no pre-propose module is set, and thus anyone can + // propose. + open_proposal_submission: + !m.prePropose || 'anyone' in m.prePropose.submissionPolicy, + proposal_module_code_hash: + (isSecretSingle + ? allCodeHashes?.[m.version]?.DaoProposalSingle + : allCodeHashes?.[m.version]?.DaoProposalMultiple) || '', + } + msg = _msg + } else { + const _msg: DaoPreProposeApprovalSingleInstantiateMsg = { + deposit_info: depositInfo && { + amount: depositInfo.amount, + denom: { + token: { + denom: depositInfo.denom, + }, + }, + refund_policy: depositInfo.refund_policy, + }, + extension: { + approver, + }, + ...(isFeatureSupportedByVersion( + Feature.GranularSubmissionPolicy, + m.version + ) + ? { + // If pre-propose module already exists, copy over submission + // policy. Otherwise, no pre-propose module is set, and thus + // anyone can propose. + submission_policy: m.prePropose?.submissionPolicy || { + anyone: { + denylist: [], + }, + }, + } + : { + // If pre-propose module already exists, check if open to + // anyone. Otherwise, no pre-propose module is set, and thus + // anyone can propose. + open_proposal_submission: + !m.prePropose || 'anyone' in m.prePropose.submissionPolicy, + }), + } + msg = _msg + } + + return makeExecuteSmartContractMessage({ + chainId: this.options.chain.chain_id, + contractAddress: m.address, + sender: this.options.address, + msg: { + update_pre_propose_info: { + info: { + module_may_propose: { + info: isSecret + ? ({ + admin: { core_module: {} }, + code_hash: codeHash || '', + code_id: codeId, + funds: [], + label: `dao-pre-propose-approval-${ + isSingle ? 'single' : 'multiple' + }_${Date.now()}`, + msg: encodeJsonToBase64(msg), + } as SecretModuleInstantiateInfo) + : ({ + admin: { core_module: {} }, + code_id: codeId, + label: `dao-pre-propose-approval-${ + isSingle ? 'single' : 'multiple' + }_${Date.now()}`, + msg: encodeJsonToBase64(msg), + // Conditionally include funds field. + ...(isFeatureSupportedByVersion( + Feature.ModuleInstantiateFunds, + m.version + ) && { + funds: [], + }), + } as ModuleInstantiateInfo), + }, + } as PreProposeInfo, + }, + }, + }) + }) + ) + } + + match(messages: ProcessedMessage[]): ActionMatch { + // Type-check. This is already checked in the constructor. + if ( + this.options.context.type !== ActionContextType.Dao || + this.options.chainContext.type !== ActionChainContextType.Supported + ) { + throw new Error('Invalid context for enabling approver') + } + + const { allCodeIds } = this.options.chainContext.config + + let count = 0 + let approver = '' + + for (const { decodedMessage } of messages) { + if ( + objectMatchesStructure(decodedMessage, { + wasm: { + execute: { + contract_addr: {}, + msg: { + update_pre_propose_info: { + info: { + module_may_propose: { + info: { + admin: {}, + code_id: {}, + label: {}, + msg: {}, + }, + }, + }, + }, + }, + }, + }, + }) && + // Ensure expected label + decodedMessage.wasm.execute.msg.update_pre_propose_info.info.module_may_propose.info.label.startsWith( + 'dao-pre-propose-approval-' + ) && + // Ensure supported code ID + Object.values(allCodeIds).some( + (config) => + // TODO(approver-multiple): also check for DaoPreProposeApprovalMultiple + config.DaoPreProposeApprovalSingle === + decodedMessage.wasm.execute.msg.update_pre_propose_info.info + .module_may_propose.info.code_id + ) + ) { + const decodedInstantiate = decodeJsonFromBase64( + decodedMessage.wasm.execute.msg.update_pre_propose_info.info + .module_may_propose.info.msg + ) + if ( + objectMatchesStructure(decodedInstantiate, { + extension: { + approver: {}, + }, + }) + ) { + // Ensure approver is the same for every message, or else we should + // render a separate action for each different approver. Set approver + // on the first message match, and validate for the rest. + if (count === 0) { + approver = decodedInstantiate.extension.approver + } else if (approver !== decodedInstantiate.extension.approver) { + break + } + count += 1 + } + } + } + + return count + } + + // The match function ensures all messages have the same approver, so we only + // need to decode one. + decode([{ decodedMessage }]: ProcessedMessage[]): EnableApproverData { + const decodedInstantiate = decodeJsonFromBase64( + decodedMessage.wasm.execute.msg.update_pre_propose_info.info + .module_may_propose.info.msg + ) + + return { + approver: decodedInstantiate.extension.approver, + } + } +} diff --git a/packages/stateful/actions/core/actions/index.ts b/packages/stateful/actions/core/actions/index.ts index ae708f70d..d49357341 100644 --- a/packages/stateful/actions/core/actions/index.ts +++ b/packages/stateful/actions/core/actions/index.ts @@ -18,6 +18,7 @@ export * from './CreateValenceAccount' export * from './CrossChainExecute' export * from './Custom' export * from './DaoAdminExec' +export * from './EnableApprover' export * from './EnableMultipleChoice' export * from './EnableRetroactiveCompensation' export * from './Execute' diff --git a/packages/stateful/actions/core/categories/dao-governance.ts b/packages/stateful/actions/core/categories/dao-governance.ts index 001e0c0dc..e209b7ec2 100644 --- a/packages/stateful/actions/core/categories/dao-governance.ts +++ b/packages/stateful/actions/core/categories/dao-governance.ts @@ -19,7 +19,6 @@ export const makeDaoGovernanceActionCategory: ActionCategoryMaker = ({ ActionKey.DaoAdminExec, ActionKey.UpgradeV1ToV2, ActionKey.CreateCrossChainAccount, - ActionKey.BecomeApprover, ActionKey.VetoProposal, ActionKey.ExecuteProposal, ActionKey.ManageVetoableDaos, @@ -28,5 +27,7 @@ export const makeDaoGovernanceActionCategory: ActionCategoryMaker = ({ ActionKey.UpdateProposalConfig, ActionKey.UpdatePreProposeConfig, ActionKey.CreateDao, + ActionKey.EnableApprover, + ActionKey.BecomeApprover, ], }) diff --git a/packages/stateful/clients/proposal-module/MultipleChoiceProposalModule.secret.ts b/packages/stateful/clients/proposal-module/MultipleChoiceProposalModule.secret.ts index d9782d995..e6a73425c 100644 --- a/packages/stateful/clients/proposal-module/MultipleChoiceProposalModule.secret.ts +++ b/packages/stateful/clients/proposal-module/MultipleChoiceProposalModule.secret.ts @@ -419,7 +419,7 @@ export class SecretMultipleChoiceProposalModule extends ProposalModuleBase< // Convert snip20 to cw20 key. 'snip20' in depositInfo.denom ? { - // Code hash. + // [address, code hash] cw20: depositInfo.denom.snip20[0], } : depositInfo.denom, diff --git a/packages/stateful/clients/proposal-module/SingleChoiceProposalModule.secret.ts b/packages/stateful/clients/proposal-module/SingleChoiceProposalModule.secret.ts index 47c9a7d53..3377c551c 100644 --- a/packages/stateful/clients/proposal-module/SingleChoiceProposalModule.secret.ts +++ b/packages/stateful/clients/proposal-module/SingleChoiceProposalModule.secret.ts @@ -194,7 +194,9 @@ export class SecretSingleChoiceProposalModule extends ProposalModuleBase< }) isPreProposeApprovalProposal = - this.prePropose.contractName === ContractName.PreProposeApprovalSingle + this.prePropose.contractName === + ContractName.PreProposeApprovalSingle || + this.prePropose.contractName === ContractName.PreProposeApprovalMultiple proposalNumber = // pre-propose-approval proposals have a different event isPreProposeApprovalProposal @@ -438,7 +440,7 @@ export class SecretSingleChoiceProposalModule extends ProposalModuleBase< // Convert snip20 to cw20 key. 'snip20' in depositInfo.denom ? { - // Code hash. + // [address, code hash] cw20: depositInfo.denom.snip20[0], } : depositInfo.denom, diff --git a/packages/stateful/clients/proposal-module/SingleChoiceProposalModule.ts b/packages/stateful/clients/proposal-module/SingleChoiceProposalModule.ts index 97dd8ef13..382e3e258 100644 --- a/packages/stateful/clients/proposal-module/SingleChoiceProposalModule.ts +++ b/packages/stateful/clients/proposal-module/SingleChoiceProposalModule.ts @@ -241,7 +241,9 @@ export class SingleChoiceProposalModule extends ProposalModuleBase< ) isPreProposeApprovalProposal = - this.prePropose.contractName === ContractName.PreProposeApprovalSingle + this.prePropose.contractName === + ContractName.PreProposeApprovalSingle || + this.prePropose.contractName === ContractName.PreProposeApprovalMultiple proposalNumber = // pre-propose-approval proposals have a different event isPreProposeApprovalProposal diff --git a/packages/types/actions.ts b/packages/types/actions.ts index 362624c14..71b506366 100644 --- a/packages/types/actions.ts +++ b/packages/types/actions.ts @@ -67,6 +67,7 @@ export enum ActionKey { EnableRetroactiveCompensation = 'enableRetroactiveCompensation', DaoAdminExec = 'daoAdminExec', EnableMultipleChoice = 'enableMultipleChoice', + EnableApprover = 'enableApprover', BecomeApprover = 'becomeApprover', ManageWidgets = 'manageWidgets', FeeShare = 'feeShare', diff --git a/packages/types/dao.ts b/packages/types/dao.ts index 02ed00f82..88170469b 100644 --- a/packages/types/dao.ts +++ b/packages/types/dao.ts @@ -114,12 +114,31 @@ export type DaoSource = { } export enum PreProposeModuleType { + /** + * The normal 'pre-propose-single' or 'pre-propose-multiple' module. + */ + Normal = 'normal', + /** + * The 'pre-propose-approval-single' or 'pre-propose-approval-multiple' + * module. + */ Approval = 'approval', + /** + * The 'pre-propose-approver' module, attached to an approval pre-propose + * module in another DAO. + */ Approver = 'approver', - // Neutron fork SubDAOs use timelock. + /** + * Neutron fork SubDAOs use timelock. + */ NeutronSubdaoSingle = 'neutron_subdao_single', - // Neutron fork DAO uses overrule pre-propose paired with SubDAO timelocks. + /** + * Neutron fork DAO uses overrule pre-propose paired with SubDAO timelocks. + */ NeutronOverruleSingle = 'neutron_overrule_single', + /** + * An unrecognized pre-propose module. + */ Other = 'other', } @@ -150,6 +169,10 @@ export type PreProposeModuleNeutronSubdaoSingleConfig = { } export type PreProposeModuleTypedConfig = + | { + type: PreProposeModuleType.Normal + config?: undefined + } | { type: PreProposeModuleType.Approval config: PreProposeModuleApprovalConfig diff --git a/packages/utils/constants/contracts.ts b/packages/utils/constants/contracts.ts index 2c3b35471..8385a65e7 100644 --- a/packages/utils/constants/contracts.ts +++ b/packages/utils/constants/contracts.ts @@ -8,7 +8,9 @@ export enum ContractName { CwTokenfactoryIssuer = 'cw-tokenfactory-issuer', PolytoneProxy = 'crates.io:polytone-proxy', PreProposeSingle = 'crates.io:dao-pre-propose-single', + PreProposeMultiple = 'crates.io:dao-pre-propose-multiple', PreProposeApprovalSingle = 'crates.io:dao-pre-propose-approval-single', + PreProposeApprovalMultiple = 'crates.io:dao-pre-propose-approval-multiple', PreProposeApprover = 'crates.io:dao-pre-propose-approver', NeutronCwdSubdaoCore = 'crates.io:cwd-subdao-core', NeutronCwdSubdaoPreProposeSingle = 'crates.io:cwd-subdao-pre-propose-single', @@ -101,7 +103,7 @@ export const DAO_PROPOSAL_SINGLE_CONTRACT_NAMES = [ ] export const DAO_PRE_PROPOSE_MULTIPLE_CONTRACT_NAMES = [ - 'crates.io:dao-pre-propose-multiple', + ContractName.PreProposeMultiple, ] export const DAO_PROPOSAL_MULTIPLE_CONTRACT_NAMES = [ diff --git a/packages/utils/features.ts b/packages/utils/features.ts index b61bc3c1d..2e18a8598 100644 --- a/packages/utils/features.ts +++ b/packages/utils/features.ts @@ -61,7 +61,8 @@ export const getSupportedFeatures = ( * * @param {ContractVersion} version - The version to check. * @param {ContractVersion} gteThis - The version to compare with. - * @return {boolean} Returns true if the given version is higher than the specified version, otherwise false. + * @return {boolean} Returns true if the given version is greater than or equal + * to the specified version. */ export const versionGte = ( version: ContractVersion, diff --git a/packages/utils/scripts/verify-code-hashes.ts b/packages/utils/scripts/verify-code-hashes.ts index fa2b33a66..3235f5bfb 100644 --- a/packages/utils/scripts/verify-code-hashes.ts +++ b/packages/utils/scripts/verify-code-hashes.ts @@ -5,6 +5,8 @@ import chalk from 'chalk' +import { ContractVersion } from '@dao-dao/types' + import { getCosmWasmClientForChainId } from '../client' import { SUPPORTED_CHAINS } from '../constants/chains' import { retry } from '../network' @@ -22,7 +24,7 @@ const main = async () => { const client = await getCosmWasmClientForChainId(chainId) for (const [version, codeHashes] of Object.entries(allCodeHashes)) { - const codeIds = allCodeIds[version] + const codeIds = allCodeIds[version as ContractVersion] if (!codeIds) { continue } @@ -30,7 +32,7 @@ const main = async () => { for (const [key, codeHash] of Object.entries(codeHashes)) { const id = `${chainId}:${key}:${version}` - const codeId = codeIds[key] + const codeId = codeIds[key as keyof typeof codeIds] if (!codeId) { console.log(chalk.red(`${id} no code ID found`)) continue