diff --git a/.agents/shared/metrics/hits.jsonl b/.agents/shared/metrics/hits.jsonl index 65d099b71b..91b7d5b451 100644 --- a/.agents/shared/metrics/hits.jsonl +++ b/.agents/shared/metrics/hits.jsonl @@ -146,3 +146,11 @@ {"ts":"2026-07-28T16:26:08.696Z","tool":"Edit","file":"apps/app/src/shared/components/transactionDialog/useManagedTransaction.ts","rule":"error-and-monitoring","bytes":2525,"elapsed_ms":2,"adapter":"claude"} {"ts":"2026-07-28T16:26:28.132Z","tool":"Edit","file":"apps/app/src/shared/components/transactionDialog/useManagedTransaction.test.ts","rule":"error-and-monitoring","bytes":2525,"elapsed_ms":1,"adapter":"claude"} {"ts":"2026-07-28T16:31:25.874Z","tool":"Edit","file":"apps/app/src/shared/components/transactionDialog/transactionDialog.tsx","rule":"error-and-monitoring","bytes":2525,"elapsed_ms":1,"adapter":"claude"} +{"ts":"2026-08-03T13:05:06.523Z","tool":"Edit","file":"apps/app/src/modules/governance/api/smartContractService/smartContractService.api.ts","rule":"query-and-cache","bytes":2385,"elapsed_ms":3,"adapter":"claude"} +{"ts":"2026-08-03T13:05:14.497Z","tool":"Edit","file":"apps/app/src/modules/governance/api/smartContractService/smartContractService.api.ts","rule":"query-and-cache","bytes":2385,"elapsed_ms":11,"adapter":"claude"} +{"ts":"2026-08-03T13:05:27.682Z","tool":"Edit","file":"apps/app/src/modules/governance/api/smartContractService/smartContractService.api.ts","rule":"query-and-cache","bytes":2385,"elapsed_ms":3,"adapter":"claude"} +{"ts":"2026-08-03T13:05:34.336Z","tool":"Edit","file":"apps/app/src/modules/governance/api/smartContractService/smartContractService.ts","rule":"query-and-cache","bytes":2385,"elapsed_ms":3,"adapter":"claude"} +{"ts":"2026-08-03T13:13:29.614Z","tool":"Edit","file":"apps/app/src/modules/governance/api/smartContractService/smartContractService.api.ts","rule":"query-and-cache","bytes":2385,"elapsed_ms":3,"adapter":"claude"} +{"ts":"2026-08-03T13:13:36.306Z","tool":"Edit","file":"apps/app/src/modules/governance/api/smartContractService/smartContractService.api.ts","rule":"query-and-cache","bytes":2385,"elapsed_ms":3,"adapter":"claude"} +{"ts":"2026-08-03T14:29:17.284Z","tool":"Edit","file":"apps/app/src/actions/core/createProposal/createProposalActionDetails.tsx","rule":"core-action-decoded-input","bytes":948,"elapsed_ms":3,"adapter":"claude"} +{"ts":"2026-08-03T14:29:22.784Z","tool":"Edit","file":"apps/app/src/actions/core/execute/executeActionDetails.tsx","rule":"core-action-decoded-input","bytes":948,"elapsed_ms":2,"adapter":"claude"} diff --git a/.changeset/app-1057-enable-actioncomposer-outside-dao-context.md b/.changeset/app-1057-enable-actioncomposer-outside-dao-context.md new file mode 100644 index 0000000000..6d60fa8d08 --- /dev/null +++ b/.changeset/app-1057-enable-actioncomposer-outside-dao-context.md @@ -0,0 +1,5 @@ +--- +"@aragon/app": minor +--- + +Enable ActionComposer to work outside of a DAO context by accepting a network prop diff --git a/apps/app/src/actions/capitalDistributor/components/capitalDistributorEndCampaignActionCreate/capitalDistributorEndCampaignActionCreate.tsx b/apps/app/src/actions/capitalDistributor/components/capitalDistributorEndCampaignActionCreate/capitalDistributorEndCampaignActionCreate.tsx index ab9b44906c..13ef7e58be 100644 --- a/apps/app/src/actions/capitalDistributor/components/capitalDistributorEndCampaignActionCreate/capitalDistributorEndCampaignActionCreate.tsx +++ b/apps/app/src/actions/capitalDistributor/components/capitalDistributorEndCampaignActionCreate/capitalDistributorEndCampaignActionCreate.tsx @@ -36,6 +36,12 @@ export const CapitalDistributorEndCampaignActionCreate: React.FC< ICapitalDistributorEndCampaignActionCreateProps > = (props) => { const { action, index } = props; + + // The view resolves its DAO data from the action, so it only supports actions composed in DAO context. + invariant( + action.daoId != null, + 'CapitalDistributorEndCampaignActionCreate: daoId must be set on the action.', + ); const { t } = useTranslations(); const { open } = useDialogContext(); const { setValue, resetField } = useFormContext(); diff --git a/apps/app/src/actions/capitalDistributor/components/capitalDistributorPauseCampaignActionCreate/capitalDistributorPauseCampaignActionCreate.tsx b/apps/app/src/actions/capitalDistributor/components/capitalDistributorPauseCampaignActionCreate/capitalDistributorPauseCampaignActionCreate.tsx index 72872ec2d3..4523fc60aa 100644 --- a/apps/app/src/actions/capitalDistributor/components/capitalDistributorPauseCampaignActionCreate/capitalDistributorPauseCampaignActionCreate.tsx +++ b/apps/app/src/actions/capitalDistributor/components/capitalDistributorPauseCampaignActionCreate/capitalDistributorPauseCampaignActionCreate.tsx @@ -36,6 +36,12 @@ export const CapitalDistributorPauseCampaignActionCreate: React.FC< ICapitalDistributorPauseCampaignActionCreateProps > = (props) => { const { action, index } = props; + + // The view resolves its DAO data from the action, so it only supports actions composed in DAO context. + invariant( + action.daoId != null, + 'CapitalDistributorPauseCampaignActionCreate: daoId must be set on the action.', + ); const { t } = useTranslations(); const { open } = useDialogContext(); const { setValue, resetField } = useFormContext(); diff --git a/apps/app/src/actions/capitalDistributor/components/capitalDistributorResumeCampaignActionCreate/capitalDistributorResumeCampaignActionCreate.tsx b/apps/app/src/actions/capitalDistributor/components/capitalDistributorResumeCampaignActionCreate/capitalDistributorResumeCampaignActionCreate.tsx index c575089355..995e30d7da 100644 --- a/apps/app/src/actions/capitalDistributor/components/capitalDistributorResumeCampaignActionCreate/capitalDistributorResumeCampaignActionCreate.tsx +++ b/apps/app/src/actions/capitalDistributor/components/capitalDistributorResumeCampaignActionCreate/capitalDistributorResumeCampaignActionCreate.tsx @@ -36,6 +36,12 @@ export const CapitalDistributorResumeCampaignActionCreate: React.FC< ICapitalDistributorResumeCampaignActionCreateProps > = (props) => { const { action, index } = props; + + // The view resolves its DAO data from the action, so it only supports actions composed in DAO context. + invariant( + action.daoId != null, + 'CapitalDistributorResumeCampaignActionCreate: daoId must be set on the action.', + ); const { t } = useTranslations(); const { open } = useDialogContext(); const { setValue, resetField } = useFormContext(); diff --git a/apps/app/src/actions/core/createProposal/createProposalActionDetails.tsx b/apps/app/src/actions/core/createProposal/createProposalActionDetails.tsx index 03d70972d8..59b0c340ff 100644 --- a/apps/app/src/actions/core/createProposal/createProposalActionDetails.tsx +++ b/apps/app/src/actions/core/createProposal/createProposalActionDetails.tsx @@ -8,6 +8,7 @@ import { InputContainer, type IProposalAction, type IProposalActionComponentProps, + invariant, } from '@aragon/gov-ui-kit'; import type { IProposalActionData } from '@/modules/governance/components/createProposalForm'; import { NestedActionsList } from '@/modules/governance/components/nestedActionsList'; @@ -25,6 +26,13 @@ export const CreateProposalActionDetails: React.FC< ICreateProposalActionDetailsProps > = (props) => { const { action, chainId } = props; + + // The view resolves its DAO data from the action, so it only supports actions composed in DAO context. + invariant( + action.daoId != null, + 'CreateProposalActionDetails: daoId must be set on the action.', + ); + const { inputData } = action as unknown as ICoreActionCreateProposal; const { t } = useTranslations(); diff --git a/apps/app/src/actions/core/execute/executeActionDetails.tsx b/apps/app/src/actions/core/execute/executeActionDetails.tsx index cb9028691f..82df1b2675 100644 --- a/apps/app/src/actions/core/execute/executeActionDetails.tsx +++ b/apps/app/src/actions/core/execute/executeActionDetails.tsx @@ -4,6 +4,7 @@ import { InputContainer, type IProposalAction, type IProposalActionComponentProps, + invariant, } from '@aragon/gov-ui-kit'; import type { IProposalActionData } from '@/modules/governance/components/createProposalForm'; import { NestedActionsList } from '@/modules/governance/components/nestedActionsList'; @@ -19,6 +20,13 @@ export const ExecuteActionDetails: React.FC = ( props, ) => { const { action, chainId } = props; + + // The view resolves its DAO data from the action, so it only supports actions composed in DAO context. + invariant( + action.daoId != null, + 'ExecuteActionDetails: daoId must be set on the action.', + ); + const { inputData } = action as unknown as ICoreActionExecute; const { t } = useTranslations(); diff --git a/apps/app/src/actions/gaugeRegistrar/components/gaugeRegistrarUnregisterGaugeActionCreate/gaugeRegistrarUnregisterGaugeActionCreate.tsx b/apps/app/src/actions/gaugeRegistrar/components/gaugeRegistrarUnregisterGaugeActionCreate/gaugeRegistrarUnregisterGaugeActionCreate.tsx index c9e12da801..9959b7a010 100644 --- a/apps/app/src/actions/gaugeRegistrar/components/gaugeRegistrarUnregisterGaugeActionCreate/gaugeRegistrarUnregisterGaugeActionCreate.tsx +++ b/apps/app/src/actions/gaugeRegistrar/components/gaugeRegistrarUnregisterGaugeActionCreate/gaugeRegistrarUnregisterGaugeActionCreate.tsx @@ -37,6 +37,12 @@ export const GaugeRegistrarUnregisterGaugeActionCreate: React.FC< IGaugeRegistrarUnregisterGaugeActionCreateProps > = (props) => { const { action, index } = props; + + // The view resolves its DAO data from the action, so it only supports actions composed in DAO context. + invariant( + action.daoId != null, + 'GaugeRegistrarUnregisterGaugeActionCreate: daoId must be set on the action.', + ); const { t } = useTranslations(); const { open } = useDialogContext(); const { setValue } = useFormContext(); diff --git a/apps/app/src/actions/gaugeRegistrar/components/gaugeRegistrarUnregisterGaugeActionDetails/gaugeRegistrarUnregisterGaugeActionDetails.tsx b/apps/app/src/actions/gaugeRegistrar/components/gaugeRegistrarUnregisterGaugeActionDetails/gaugeRegistrarUnregisterGaugeActionDetails.tsx index 45035dc552..ee0f72eff9 100644 --- a/apps/app/src/actions/gaugeRegistrar/components/gaugeRegistrarUnregisterGaugeActionDetails/gaugeRegistrarUnregisterGaugeActionDetails.tsx +++ b/apps/app/src/actions/gaugeRegistrar/components/gaugeRegistrarUnregisterGaugeActionDetails/gaugeRegistrarUnregisterGaugeActionDetails.tsx @@ -7,6 +7,7 @@ import { type IProposalAction, type IProposalActionComponentProps, type IProposalActionInputDataParameter, + invariant, } from '@aragon/gov-ui-kit'; import type { Address, Hex } from 'viem'; import { useReadContract } from 'wagmi'; @@ -54,6 +55,12 @@ export const GaugeRegistrarUnregisterGaugeActionDetails: React.FC< IGaugeRegistrarUnregisterGaugeActionDetailsProps > = (props) => { const { action } = props; + + // The view resolves its DAO data from the action, so it only supports actions composed in DAO context. + invariant( + action.daoId != null, + 'GaugeRegistrarUnregisterGaugeActionDetails: daoId must be set on the action.', + ); const pluginAddress = action.to; const { data: dao } = useDao({ urlParams: { id: action.daoId } }); const [gaugeVoterPlugin] = diff --git a/apps/app/src/actions/gaugeVoter/components/gaugeVoterActivateGaugeActionCreate/gaugeVoterActivateGaugeActionCreate.tsx b/apps/app/src/actions/gaugeVoter/components/gaugeVoterActivateGaugeActionCreate/gaugeVoterActivateGaugeActionCreate.tsx index 4f413e0ce7..7b5bcf5199 100644 --- a/apps/app/src/actions/gaugeVoter/components/gaugeVoterActivateGaugeActionCreate/gaugeVoterActivateGaugeActionCreate.tsx +++ b/apps/app/src/actions/gaugeVoter/components/gaugeVoterActivateGaugeActionCreate/gaugeVoterActivateGaugeActionCreate.tsx @@ -36,6 +36,12 @@ export const GaugeVoterActivateGaugeActionCreate: React.FC< IGaugeVoterActivateGaugeActionCreateProps > = (props) => { const { action, index } = props; + + // The view resolves its DAO data from the action, so it only supports actions composed in DAO context. + invariant( + action.daoId != null, + 'GaugeVoterActivateGaugeActionCreate: daoId must be set on the action.', + ); const { t } = useTranslations(); const { open } = useDialogContext(); const { setValue } = useFormContext(); diff --git a/apps/app/src/actions/gaugeVoter/components/gaugeVoterActivateGaugeActionDetails/gaugeVoterActivateGaugeActionDetails.tsx b/apps/app/src/actions/gaugeVoter/components/gaugeVoterActivateGaugeActionDetails/gaugeVoterActivateGaugeActionDetails.tsx index 8b6ee75601..348d6c6348 100644 --- a/apps/app/src/actions/gaugeVoter/components/gaugeVoterActivateGaugeActionDetails/gaugeVoterActivateGaugeActionDetails.tsx +++ b/apps/app/src/actions/gaugeVoter/components/gaugeVoterActivateGaugeActionDetails/gaugeVoterActivateGaugeActionDetails.tsx @@ -6,6 +6,7 @@ import { EmptyState, type IProposalAction, type IProposalActionComponentProps, + invariant, } from '@aragon/gov-ui-kit'; import type { Hex } from 'viem'; import type { IProposalActionData } from '@/modules/governance/components/createProposalForm'; @@ -27,6 +28,12 @@ export const GaugeVoterActivateGaugeActionDetails: React.FC< IGaugeVoterActivateGaugeActionDetailsProps > = (props) => { const { action } = props; + + // The view resolves its DAO data from the action, so it only supports actions composed in DAO context. + invariant( + action.daoId != null, + 'GaugeVoterActivateGaugeActionDetails: daoId must be set on the action.', + ); const pluginAddress = action.to; const { data: dao } = useDao({ urlParams: { id: action.daoId } }); const { t } = useTranslations(); diff --git a/apps/app/src/actions/gaugeVoter/components/gaugeVoterDeactivateGaugeActionCreate/gaugeVoterDeactivateGaugeActionCreate.tsx b/apps/app/src/actions/gaugeVoter/components/gaugeVoterDeactivateGaugeActionCreate/gaugeVoterDeactivateGaugeActionCreate.tsx index ef56ac5d37..ca1cdc0e09 100644 --- a/apps/app/src/actions/gaugeVoter/components/gaugeVoterDeactivateGaugeActionCreate/gaugeVoterDeactivateGaugeActionCreate.tsx +++ b/apps/app/src/actions/gaugeVoter/components/gaugeVoterDeactivateGaugeActionCreate/gaugeVoterDeactivateGaugeActionCreate.tsx @@ -36,6 +36,12 @@ export const GaugeVoterDeactivateGaugeActionCreate: React.FC< IGaugeVoterDeactivateGaugeActionCreateProps > = (props) => { const { action, index } = props; + + // The view resolves its DAO data from the action, so it only supports actions composed in DAO context. + invariant( + action.daoId != null, + 'GaugeVoterDeactivateGaugeActionCreate: daoId must be set on the action.', + ); const { t } = useTranslations(); const { open } = useDialogContext(); const { setValue } = useFormContext(); diff --git a/apps/app/src/actions/gaugeVoter/components/gaugeVoterDeactivateGaugeActionDetails/gaugeVoterDeactivateGaugeActionDetails.tsx b/apps/app/src/actions/gaugeVoter/components/gaugeVoterDeactivateGaugeActionDetails/gaugeVoterDeactivateGaugeActionDetails.tsx index 82f6be060e..7301d3bc18 100644 --- a/apps/app/src/actions/gaugeVoter/components/gaugeVoterDeactivateGaugeActionDetails/gaugeVoterDeactivateGaugeActionDetails.tsx +++ b/apps/app/src/actions/gaugeVoter/components/gaugeVoterDeactivateGaugeActionDetails/gaugeVoterDeactivateGaugeActionDetails.tsx @@ -6,6 +6,7 @@ import { EmptyState, type IProposalAction, type IProposalActionComponentProps, + invariant, } from '@aragon/gov-ui-kit'; import type { Hex } from 'viem'; import type { IProposalActionData } from '@/modules/governance/components/createProposalForm'; @@ -27,6 +28,12 @@ export const GaugeVoterDeactivateGaugeActionDetails: React.FC< IGaugeVoterDeactivateGaugeActionDetailsProps > = (props) => { const { action } = props; + + // The view resolves its DAO data from the action, so it only supports actions composed in DAO context. + invariant( + action.daoId != null, + 'GaugeVoterDeactivateGaugeActionDetails: daoId must be set on the action.', + ); const pluginAddress = action.to; const { data: dao } = useDao({ urlParams: { id: action.daoId } }); const { t } = useTranslations(); diff --git a/apps/app/src/actions/gaugeVoter/components/gaugeVoterUpdateGaugeMetadataActionCreate/gaugeVoterUpdateGaugeMetadataActionCreate.tsx b/apps/app/src/actions/gaugeVoter/components/gaugeVoterUpdateGaugeMetadataActionCreate/gaugeVoterUpdateGaugeMetadataActionCreate.tsx index 4144d4f859..1315ade62b 100644 --- a/apps/app/src/actions/gaugeVoter/components/gaugeVoterUpdateGaugeMetadataActionCreate/gaugeVoterUpdateGaugeMetadataActionCreate.tsx +++ b/apps/app/src/actions/gaugeVoter/components/gaugeVoterUpdateGaugeMetadataActionCreate/gaugeVoterUpdateGaugeMetadataActionCreate.tsx @@ -39,6 +39,12 @@ export const GaugeVoterUpdateGaugeMetadataActionCreate: React.FC< IGaugeVoterUpdateGaugeMetadataActionCreateProps > = (props) => { const { action, index, chainId } = props; + + // The view resolves its DAO data from the action, so it only supports actions composed in DAO context. + invariant( + action.daoId != null, + 'GaugeVoterUpdateGaugeMetadataActionCreate: daoId must be set on the action.', + ); const { t } = useTranslations(); const { open } = useDialogContext(); const { setValue, unregister } = useFormContext(); diff --git a/apps/app/src/modules/governance/api/smartContractService/smartContractService.api.ts b/apps/app/src/modules/governance/api/smartContractService/smartContractService.api.ts index 234bf95afe..4e37b6884e 100644 --- a/apps/app/src/modules/governance/api/smartContractService/smartContractService.api.ts +++ b/apps/app/src/modules/governance/api/smartContractService/smartContractService.api.ts @@ -1,5 +1,6 @@ import type { Network } from '@/shared/api/daoService'; import type { + IRequestBodyParams, IRequestUrlBodyParams, IRequestUrlParams, } from '@/shared/api/httpService'; @@ -51,13 +52,16 @@ export interface IDecodeTransactionParams export interface IDecodeTransactionsLightUrlParams { /** - * Network of the DAO. + * Network of the smart contracts to decode actions for. */ network: Network; +} + +export interface IDecodeTransactionsLightQueryParams { /** - * Address of the DAO. + * Address sending the actions, i.e., DAO. */ - address: string; + from: string; } export interface IDecodeTransactionsLightAction { @@ -82,7 +86,10 @@ export type IDecodeTransactionsLightBodyParams = IDecodeTransactionsLightAction[]; export interface IDecodeTransactionsLightParams - extends IRequestUrlBodyParams< - IDecodeTransactionsLightUrlParams, - IDecodeTransactionsLightBodyParams - > {} + extends IRequestUrlParams, + IRequestBodyParams { + /** + * Query parameters of the request, omitted when decoding outside DAO context. + */ + queryParams?: IDecodeTransactionsLightQueryParams; +} diff --git a/apps/app/src/modules/governance/api/smartContractService/smartContractService.ts b/apps/app/src/modules/governance/api/smartContractService/smartContractService.ts index d50c204e17..134fb443d9 100644 --- a/apps/app/src/modules/governance/api/smartContractService/smartContractService.ts +++ b/apps/app/src/modules/governance/api/smartContractService/smartContractService.ts @@ -11,7 +11,7 @@ class SmartContractService extends AragonBackendService { private urls = { abi: '/v2/contract/:network/:address', decodeTransaction: '/v2/contract/:network/:address/decode', - decodeTransactionsBatch: '/v2/contract/:network/:address/decode-batch', + decodeTransactionsBatch: '/v3/contract/:network/decode-batch', }; getAbi = async ( diff --git a/apps/app/src/modules/governance/components/actionComposer/actionComposer/actionComposer.tsx b/apps/app/src/modules/governance/components/actionComposer/actionComposer/actionComposer.tsx index dbbe4446c2..ad6861d0e2 100644 --- a/apps/app/src/modules/governance/components/actionComposer/actionComposer/actionComposer.tsx +++ b/apps/app/src/modules/governance/components/actionComposer/actionComposer/actionComposer.tsx @@ -11,8 +11,11 @@ import { } from '@aragon/gov-ui-kit'; import classNames from 'classnames'; import { useCallback, useRef, useState } from 'react'; -import type { IDaoPermission } from '@/shared/api/daoService'; -import { useDao } from '@/shared/api/daoService'; +import { + type IDaoPermission, + type Network, + useDao, +} from '@/shared/api/daoService'; import { useDialogContext } from '@/shared/components/dialogProvider'; import { useTranslations } from '@/shared/components/translationsProvider'; import type { IAllowedAction } from '../../../api/executeSelectorsService'; @@ -21,10 +24,7 @@ import type { ISmartContractAbi } from '../../../api/smartContractService'; import { GovernanceDialogId } from '../../../constants/governanceDialogId'; import type { IVerifySmartContractDialogParams } from '../../../dialogs/verifySmartContractDialog'; import type { IWalletConnectActionDialogParams } from '../../../dialogs/walletConnectActionDialog'; -import { - type IExportedAction, - proposalActionsImportExportUtils, -} from '../../../utils/proposalActionsImportExportUtils'; +import { proposalActionsImportExportUtils } from '../../../utils/proposalActionsImportExportUtils'; import type { IProposalActionData } from '../../createProposalForm'; import { ActionComposerInput, @@ -37,9 +37,15 @@ import { ActionItemId } from '../actionComposerUtils.api'; export interface IActionComposerProps extends Pick { /** - * ID of the DAO. + * ID of the DAO. When omitted the composer runs in network-only mode, outside DAO context. + * No DAO-, plugin- or permission-specific actions are offered and only the decoded action view is supported. + * Either `daoId` or `network` must be set. */ - daoId: string; + daoId?: string; + /** + * Network to build network-scoped actions for. Falls back to the DAO network when `daoId` is set. + */ + network?: Network; /** * Callback called when an action is added. * @param value - single action or array of actions to be added. @@ -84,6 +90,7 @@ export interface IActionComposerProps export const ActionComposer: React.FC = (props) => { const { daoId, + network, onAddAction, excludeActionTypes, hideWalletConnect = false, @@ -96,8 +103,18 @@ export const ActionComposer: React.FC = (props) => { hasPinErrors = false, } = props; - const daoUrlParams = { id: daoId }; - const { data: dao } = useDao({ urlParams: daoUrlParams }); + invariant( + daoId != null || network != null, + 'ActionComposer: either daoId or network must be set.', + ); + + const daoUrlParams = { id: daoId ?? '' }; + const { data: dao } = useDao( + { urlParams: daoUrlParams }, + { enabled: daoId != null }, + ); + + const resolvedNetwork = network ?? dao?.network; const { t } = useTranslations(); const { open } = useDialogContext(); @@ -148,8 +165,13 @@ export const ActionComposer: React.FC = (props) => { }; const handleVerifySmartContract = (initialValue?: string) => { + invariant( + resolvedNetwork != null, + 'handleVerifySmartContract: `resolvedNetwork` not found', + ); + const params: IVerifySmartContractDialogParams = { - network: dao!.network, + network: resolvedNetwork, onSubmit: handleAbiSubmit, initialValue, }; @@ -160,6 +182,11 @@ export const ActionComposer: React.FC = (props) => { }; const handleAddWalletConnectActions = (actions: IProposalAction[]) => { + invariant( + daoId != null, + 'handleAddWalletConnectActions: daoId is required for WalletConnect imports', + ); + const parsedActions = actions.map((action) => ({ ...action, daoId, @@ -169,22 +196,28 @@ export const ActionComposer: React.FC = (props) => { }; const displayWalletConnectDialog = () => { + invariant( + dao != null, + 'displayWalletConnectDialog: `dao` is required for WalletConnect dialog', + ); + setUploadError(null); const params: IWalletConnectActionDialogParams = { onAddActionsClick: handleAddWalletConnectActions, - daoAddress: dao!.address, - daoNetwork: dao!.network, + daoAddress: dao.address, + daoNetwork: dao.network, }; open(GovernanceDialogId.WALLET_CONNECT_ACTION, { params, stack: true }); }; - const handleImportActions = (actions: IExportedAction[]) => { + const handleImportActions = (actions: IProposalAction[]) => { const parsedActions = actions.map( (action) => ({ ...action, value: BigInt(action.value), daoId, + meta: undefined, }) as unknown as IProposalActionData, ); onAddAction(parsedActions); @@ -194,8 +227,8 @@ export const ActionComposer: React.FC = (props) => { event: React.ChangeEvent, ) => { invariant( - dao != null, - 'DAO must be defined to import actions from file', + resolvedNetwork != null, + 'handleDirectFileUpload: `resolvedNetwork` not found', ); const file = event.target.files?.[0]; @@ -218,6 +251,7 @@ export const ActionComposer: React.FC = (props) => { const decodedActions = await proposalActionsImportExportUtils.decodeActions( result.actions, + resolvedNetwork, dao, ); @@ -277,7 +311,9 @@ export const ActionComposer: React.FC = (props) => { onDownloadActions != null && onRemoveAllActions != null; const shouldRenderWalletConnect = !( - hideWalletConnect || onlyShowAuthorizedActions + daoId == null || + hideWalletConnect || + onlyShowAuthorizedActions ); const shouldRenderUpload = !onlyShowAuthorizedActions; diff --git a/apps/app/src/modules/governance/components/actionComposer/actionComposerInput/actionComposerInput.api.ts b/apps/app/src/modules/governance/components/actionComposer/actionComposerInput/actionComposerInput.api.ts index 5f8eab9e5a..cb2873abb4 100644 --- a/apps/app/src/modules/governance/components/actionComposer/actionComposerInput/actionComposerInput.api.ts +++ b/apps/app/src/modules/governance/components/actionComposer/actionComposerInput/actionComposerInput.api.ts @@ -37,7 +37,7 @@ export interface IActionComposerInputProps /** * ID of the DAO. */ - daoId: string; + daoId?: string; /** * Additional native items to be displayed. */ diff --git a/apps/app/src/modules/governance/components/actionComposer/actionComposerInput/actionComposerInput.tsx b/apps/app/src/modules/governance/components/actionComposer/actionComposerInput/actionComposerInput.tsx index 0760cc67ab..e5f92d4bd7 100644 --- a/apps/app/src/modules/governance/components/actionComposer/actionComposerInput/actionComposerInput.tsx +++ b/apps/app/src/modules/governance/components/actionComposer/actionComposerInput/actionComposerInput.tsx @@ -20,8 +20,11 @@ export const ActionComposerInput = forwardRef< ...otherProps } = props; - const daoUrlParams = { id: daoId }; - const { data: dao } = useDao({ urlParams: daoUrlParams }); + const daoUrlParams = { id: daoId ?? '' }; + const { data: dao } = useDao( + { urlParams: daoUrlParams }, + { enabled: daoId != null }, + ); const { t } = useTranslations(); diff --git a/apps/app/src/modules/governance/components/actionComposer/actionComposerUtils.ts b/apps/app/src/modules/governance/components/actionComposer/actionComposerUtils.ts index 89ba78ff61..584fac88ee 100644 --- a/apps/app/src/modules/governance/components/actionComposer/actionComposerUtils.ts +++ b/apps/app/src/modules/governance/components/actionComposer/actionComposerUtils.ts @@ -238,7 +238,7 @@ class ActionComposerUtils { dao, allowedActions, }: IGetAllowedActionBaseParams): IAutocompleteInputGroup[] => { - const daoAddress = dao!.address; + const daoAddress = dao?.address; const [daoGroup] = this.getNativeActionGroups({ t, dao, @@ -546,12 +546,16 @@ class ActionComposerUtils { ): IAutocompleteInputGroup[] => { const { t, dao, nativeGroups } = params; + if (dao == null) { + return nativeGroups; + } + return [ { - id: dao!.address, + id: dao.address, name: t('app.governance.actionComposer.nativeGroup.DAO'), - info: addressUtils.truncateAddress(dao?.address), - indexData: [dao!.address], + info: addressUtils.truncateAddress(dao.address), + indexData: [dao.address], }, ...nativeGroups, ]; @@ -562,6 +566,10 @@ class ActionComposerUtils { ): IActionComposerInputItem[] => { const { t, dao, nativeItems } = params; + if (dao == null) { + return nativeItems; + } + const transferAction = this.buildTransferNativeAction(t); const metadataUpdateAction = { id: ProposalActionType.METADATA_UPDATE, @@ -569,8 +577,8 @@ class ActionComposerUtils { `app.governance.actionComposer.nativeItem.${ProposalActionType.METADATA_UPDATE}`, ), icon: IconType.SETTINGS, - groupId: dao!.address, - defaultValue: this.buildDefaultActionMetadata(dao!), + groupId: dao.address, + defaultValue: this.buildDefaultActionMetadata(dao), }; return [transferAction, metadataUpdateAction, ...nativeItems]; diff --git a/apps/app/src/modules/governance/components/createProposalForm/createProposalFormActions/proposalActions/transferAssetAction/transferAssetAction.tsx b/apps/app/src/modules/governance/components/createProposalForm/createProposalFormActions/proposalActions/transferAssetAction/transferAssetAction.tsx index 41e556c452..9271524ffb 100644 --- a/apps/app/src/modules/governance/components/createProposalForm/createProposalFormActions/proposalActions/transferAssetAction/transferAssetAction.tsx +++ b/apps/app/src/modules/governance/components/createProposalForm/createProposalFormActions/proposalActions/transferAssetAction/transferAssetAction.tsx @@ -1,6 +1,7 @@ import { addressUtils, type IProposalActionComponentProps, + invariant, } from '@aragon/gov-ui-kit'; import { useEffect } from 'react'; import { useFormContext, useWatch } from 'react-hook-form'; @@ -47,6 +48,12 @@ export const TransferAssetAction: React.FC = ( ) => { const { action, index } = props; + // The view resolves its DAO data from the action, so it only supports actions composed in DAO context. + invariant( + action.daoId != null, + 'TransferAssetAction: daoId must be set on the action.', + ); + const { setValue, getValues } = useFormContext(); const { data: dao } = useDao({ urlParams: { id: action.daoId } }); diff --git a/apps/app/src/modules/governance/components/createProposalForm/createProposalFormDefinitions.ts b/apps/app/src/modules/governance/components/createProposalForm/createProposalFormDefinitions.ts index 73c0668fab..7ab5974c66 100644 --- a/apps/app/src/modules/governance/components/createProposalForm/createProposalFormDefinitions.ts +++ b/apps/app/src/modules/governance/components/createProposalForm/createProposalFormDefinitions.ts @@ -16,9 +16,10 @@ export type IProposalActionData< */ fieldId?: string; /** - * ID of the DAO. + * ID of the DAO. Undefined when the action is composed outside a DAO context, in which case only + * the decoded action view is available (basic views resolve their data through the DAO). */ - daoId: string; + daoId?: string; /** * Additional metadata for the action. */ diff --git a/apps/app/src/modules/governance/components/proposalActionsEditor/proposalActionsEditor.tsx b/apps/app/src/modules/governance/components/proposalActionsEditor/proposalActionsEditor.tsx index 417251dfff..b3302d3978 100644 --- a/apps/app/src/modules/governance/components/proposalActionsEditor/proposalActionsEditor.tsx +++ b/apps/app/src/modules/governance/components/proposalActionsEditor/proposalActionsEditor.tsx @@ -1,5 +1,9 @@ -import type { ProposalActionComponent } from '@aragon/gov-ui-kit'; -import { useAllDaoPermissions, useDao } from '@/shared/api/daoService'; +import { invariant, type ProposalActionComponent } from '@aragon/gov-ui-kit'; +import { + type Network, + useAllDaoPermissions, + useDao, +} from '@/shared/api/daoService'; import { useTranslations } from '@/shared/components/translationsProvider'; import { useDaoChain } from '@/shared/hooks/useDaoChain'; import { useProposalActionsField } from '../../hooks/useProposalActionsField'; @@ -10,9 +14,14 @@ import { ProposalActionsEditList } from '../proposalActionsEditList'; export interface IProposalActionsEditorProps { /** - * ID of the DAO the actions are composed for. + * ID of the DAO the actions are composed for. When omitted the editor runs outside DAO context, so + * no DAO-, plugin- or permission-specific actions are offered. Either `daoId` or `network` must be set. */ - daoId: string; + daoId?: string; + /** + * Network the actions are composed for. Falls back to the DAO network when `daoId` is set. + */ + network?: Network; /** * Action types to hide from the action composer, e.g. to stop an action from being nested into itself. */ @@ -27,11 +36,19 @@ export interface IProposalActionsEditorProps { export const ProposalActionsEditor: React.FC = ( props, ) => { - const { daoId, excludeActionTypes } = props; + const { daoId, network, excludeActionTypes } = props; + + invariant( + daoId != null || network != null, + 'ProposalActionsEditor: either daoId or network must be set.', + ); const { t } = useTranslations(); - const { data: dao } = useDao({ urlParams: { id: daoId } }); - const { chainId } = useDaoChain({ daoId }); + const { data: dao } = useDao( + { urlParams: { id: daoId ?? '' } }, + { enabled: daoId != null }, + ); + const { chainId } = useDaoChain({ daoId, network }); const { actionsMerged, @@ -40,9 +57,15 @@ export const ProposalActionsEditor: React.FC = ( getArrayControls, } = useProposalActionsField(); - const { data: daoPermissions } = useAllDaoPermissions({ - urlParams: { network: dao!.network, daoAddress: dao!.address }, - }); + const { data: daoPermissions } = useAllDaoPermissions( + { + urlParams: { + network: dao?.network as Network, + daoAddress: dao?.address ?? '', + }, + }, + { enabled: dao != null }, + ); const { pluginComponents } = actionComposerUtils.getDaoPluginActions(dao); const { components: permissionActionComponents } = @@ -60,7 +83,9 @@ export const ProposalActionsEditor: React.FC = ( ...permissionActionComponents, }; - const showActionComposer = dao != null; + // In DAO context the composer needs the DAO to build its plugin and permission actions, outside of + // it there is nothing to wait for. + const showActionComposer = daoId == null || dao != null; const hasActions = actionsMerged.length > 0; return ( @@ -77,6 +102,7 @@ export const ProposalActionsEditor: React.FC = ( daoPermissions={daoPermissions} excludeActionTypes={excludeActionTypes} hasActions={hasActions} + network={network} onAddAction={handleAddAction} onRemoveAllActions={handleRemoveAllActions} /> diff --git a/apps/app/src/modules/governance/dialogs/nestedActionsDialog/nestedActionsDialog.test.tsx b/apps/app/src/modules/governance/dialogs/nestedActionsDialog/nestedActionsDialog.test.tsx index f5bcf2ff39..72d26ffe8e 100644 --- a/apps/app/src/modules/governance/dialogs/nestedActionsDialog/nestedActionsDialog.test.tsx +++ b/apps/app/src/modules/governance/dialogs/nestedActionsDialog/nestedActionsDialog.test.tsx @@ -219,6 +219,7 @@ describe(' component', () => { ).toBeInTheDocument(); expect(decodeActionsSpy).toHaveBeenCalledWith( [{ to: '0xraw', value: '10', data: '0xcalldata' }], + DAO.network, DAO, ); diff --git a/apps/app/src/modules/governance/dialogs/nestedActionsDialog/nestedActionsDialog.tsx b/apps/app/src/modules/governance/dialogs/nestedActionsDialog/nestedActionsDialog.tsx index b79da23bfd..5b7d3bc568 100644 --- a/apps/app/src/modules/governance/dialogs/nestedActionsDialog/nestedActionsDialog.tsx +++ b/apps/app/src/modules/governance/dialogs/nestedActionsDialog/nestedActionsDialog.tsx @@ -95,6 +95,7 @@ export const NestedActionsDialog: React.FC = ( value, data, })), + dao.network, dao, ); @@ -157,6 +158,7 @@ export const NestedActionsDialog: React.FC = ( } }; + // TODO: enable running without DAO ID return ( diff --git a/apps/app/src/modules/governance/utils/proposalActionsImportExportUtils/proposalActionsImportExportUtils.ts b/apps/app/src/modules/governance/utils/proposalActionsImportExportUtils/proposalActionsImportExportUtils.ts index 90acfb7db5..4b099bd236 100644 --- a/apps/app/src/modules/governance/utils/proposalActionsImportExportUtils/proposalActionsImportExportUtils.ts +++ b/apps/app/src/modules/governance/utils/proposalActionsImportExportUtils/proposalActionsImportExportUtils.ts @@ -14,6 +14,7 @@ import { tokenSettingsUtils } from '@/plugins/tokenPlugin/utils/tokenSettingsUti import { type IDao, type IDaoPlugin, + type Network, PluginInterfaceType, } from '@/shared/api/daoService'; import { smartContractService } from '../../api/smartContractService'; @@ -134,24 +135,18 @@ class ProposalActionsImportExportUtils { }; /** - * Decodes imported actions using the smart contract service - * - * @param actions - Array of exported actions to decode - * @param network - Network where the contracts exist - * @param daoAddress - DAO address - * @returns Promise resolving to array of decoded proposal actions + * Decodes imported actions using the smart contract service. The DAO is optional to support decoding + * outside a DAO context, in which case all actions are normalized to the decoded view (`Unknown` type). */ decodeActions = async ( actions: IExportedAction[], - dao: IDao, + network: Network, + dao?: IDao, ): Promise => { - const { network, address: daoAddress } = dao; const decodedActions = await smartContractService.decodeTransactionsLight({ - urlParams: { - network, - address: daoAddress, - }, + urlParams: { network }, + queryParams: dao != null ? { from: dao.address } : undefined, body: actions, }); @@ -163,8 +158,16 @@ class ProposalActionsImportExportUtils { */ normalizeDecodedActions = ( decodedActions: IProposalAction[], - dao: IDao, + dao?: IDao, ): IProposalAction[] => { + if (dao == null) { + // Without a DAO there is only decoded view available. + return decodedActions.map((action) => ({ + ...action, + type: 'Unknown', + })); + } + const { plugins } = dao; return decodedActions.map((action) => { diff --git a/apps/app/src/plugins/multisigPlugin/components/multisigActions/multisigAddMembersAction/multisigAddMembersAction.tsx b/apps/app/src/plugins/multisigPlugin/components/multisigActions/multisigAddMembersAction/multisigAddMembersAction.tsx index b408a866c6..6bb3287169 100644 --- a/apps/app/src/plugins/multisigPlugin/components/multisigActions/multisigAddMembersAction/multisigAddMembersAction.tsx +++ b/apps/app/src/plugins/multisigPlugin/components/multisigActions/multisigAddMembersAction/multisigAddMembersAction.tsx @@ -1,6 +1,7 @@ import { addressUtils, type IProposalActionComponentProps, + invariant, } from '@aragon/gov-ui-kit'; import { useEffect, useMemo } from 'react'; import { useFormContext, useWatch } from 'react-hook-form'; @@ -37,6 +38,12 @@ export const MultisigAddMembersAction: React.FC< > = (props) => { const { index, action } = props; + // The view resolves its DAO data from the action, so it only supports actions composed in DAO context. + invariant( + action.daoId != null, + 'MultisigAddMembersAction: daoId must be set on the action.', + ); + const { setValue } = useFormContext(); const actionFieldName = `actions.[${index.toString()}]`; diff --git a/apps/app/src/plugins/multisigPlugin/components/multisigActions/multisigRemoveMembersAction/multisigRemoveMembersAction.tsx b/apps/app/src/plugins/multisigPlugin/components/multisigActions/multisigRemoveMembersAction/multisigRemoveMembersAction.tsx index f144e0b48e..a43d93445f 100644 --- a/apps/app/src/plugins/multisigPlugin/components/multisigActions/multisigRemoveMembersAction/multisigRemoveMembersAction.tsx +++ b/apps/app/src/plugins/multisigPlugin/components/multisigActions/multisigRemoveMembersAction/multisigRemoveMembersAction.tsx @@ -1,6 +1,7 @@ import { addressUtils, type IProposalActionComponentProps, + invariant, } from '@aragon/gov-ui-kit'; import { useEffect, useMemo } from 'react'; import { useFormContext, useWatch } from 'react-hook-form'; @@ -38,6 +39,13 @@ export const MultisigRemoveMembersAction: React.FC< IMultisigRemoveMembersActionProps > = (props) => { const { action, index } = props; + const { daoId } = action; + + // The view resolves its DAO data from the action, so it only supports actions composed in DAO context. + invariant( + daoId != null, + 'MultisigRemoveMembersAction: daoId must be set on the action.', + ); const { setValue } = useFormContext(); const { open } = useDialogContext(); @@ -79,7 +87,7 @@ export const MultisigRemoveMembersAction: React.FC< const handleAddClick = () => { const params: IMultisigRemoveMembersActionDialogParams = { - daoId: action.daoId, + daoId, pluginAddress: action.to, onMemberClick: handleMemberClick, }; diff --git a/apps/app/src/plugins/multisigPlugin/components/multisigActions/multisigUpdateSettingsAction/multisigUpdateSettingsAction.tsx b/apps/app/src/plugins/multisigPlugin/components/multisigActions/multisigUpdateSettingsAction/multisigUpdateSettingsAction.tsx index cc27b36ce2..c9c08e800f 100644 --- a/apps/app/src/plugins/multisigPlugin/components/multisigActions/multisigUpdateSettingsAction/multisigUpdateSettingsAction.tsx +++ b/apps/app/src/plugins/multisigPlugin/components/multisigActions/multisigUpdateSettingsAction/multisigUpdateSettingsAction.tsx @@ -1,4 +1,7 @@ -import type { IProposalActionComponentProps } from '@aragon/gov-ui-kit'; +import { + type IProposalActionComponentProps, + invariant, +} from '@aragon/gov-ui-kit'; import { useEffect } from 'react'; import { useFormContext, useWatch } from 'react-hook-form'; import { encodeFunctionData } from 'viem'; @@ -50,6 +53,12 @@ export const MultisigUpdateSettingsAction: React.FC< > = (props) => { const { index, action } = props; + // The view resolves its DAO data from the action, so it only supports actions composed in DAO context. + invariant( + action.daoId != null, + 'MultisigUpdateSettingsAction: daoId must be set on the action.', + ); + const { setValue } = useFormContext(); const actionFieldName = `actions.[${index.toString()}]`;