From 5ee5042240d2e6a67491b9ecb826dfded54716f7 Mon Sep 17 00:00:00 2001 From: Evan Aronson <93671071+evanaronson@users.noreply.github.com> Date: Wed, 17 Sep 2025 18:08:26 +0200 Subject: [PATCH 1/6] feat: Add Lock To Vote plugin support for ZKsync and ZKsync Sepolia (#764) * Update lockToVotePlugin.ts adding zksync and zksync sepolia Signed-off-by: Evan Aronson <93671071+evanaronson@users.noreply.github.com> * Create weird-chains-break.md Signed-off-by: Evan Aronson <93671071+evanaronson@users.noreply.github.com> --------- Signed-off-by: Evan Aronson <93671071+evanaronson@users.noreply.github.com> --- .changeset/weird-chains-break.md | 5 +++++ src/plugins/lockToVotePlugin/constants/lockToVotePlugin.ts | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/weird-chains-break.md diff --git a/.changeset/weird-chains-break.md b/.changeset/weird-chains-break.md new file mode 100644 index 0000000000..bdb3eb6d54 --- /dev/null +++ b/.changeset/weird-chains-break.md @@ -0,0 +1,5 @@ +--- +'@aragon/app': patch +--- + +Add Lock To Vote plugin support for ZKsync and ZKsync Sepolia diff --git a/src/plugins/lockToVotePlugin/constants/lockToVotePlugin.ts b/src/plugins/lockToVotePlugin/constants/lockToVotePlugin.ts index 9bbb194210..f48b4ada3e 100644 --- a/src/plugins/lockToVotePlugin/constants/lockToVotePlugin.ts +++ b/src/plugins/lockToVotePlugin/constants/lockToVotePlugin.ts @@ -17,8 +17,8 @@ export const lockToVotePlugin: IPluginInfo = { [Network.POLYGON_MAINNET]: '0x326D2b4cC92281D6fF757D79af98bE255BA45cE1', [Network.BASE_MAINNET]: '0x05ECA5ab78493Bf812052B0211a206BCBA03471B', [Network.ARBITRUM_MAINNET]: '0xe92eF55cCbB3ac48f54f2FcDC4c49379CB01C57F', - [Network.ZKSYNC_MAINNET]: '0x0000000000000000000000000000000000000000', - [Network.ZKSYNC_SEPOLIA]: '0x0000000000000000000000000000000000000000', + [Network.ZKSYNC_MAINNET]: '0xd0f0Bc285F4D27417ECd8C027BB6746690ba72b2', + [Network.ZKSYNC_SEPOLIA]: '0x8e9E356D9a0a99a88b22C1913D3891985959b3A8', [Network.PEAQ_MAINNET]: '0x077F72D7676483D9778439AA8d58dbDf8DD80a82', [Network.OPTIMISM_MAINNET]: '0x306E4339aE3bd3ba7Fd1BB32c8b4d3A5cd90f379', [Network.CORN_MAINNET]: '0xc6E2b94A9E75a0ddEF9577Dc90B3BC4aBa8c29c9', From 34425b8c8a67ebaed4e8c9d43d81eafb11696a57 Mon Sep 17 00:00:00 2001 From: "heykd.eth" <65736142+thekidnamedkd@users.noreply.github.com> Date: Wed, 17 Sep 2025 18:15:35 +0200 Subject: [PATCH 2/6] fix(APP-4553): Implement unique enum ID for LockToVote dialog --- .changeset/fancy-moments-cross.md | 5 +++++ .../lockToVotePlugin/constants/lockToVotePluginDialogId.ts | 2 +- .../constants/lockToVotePluginDialogsDefinitions.ts | 2 +- .../hooks/useLockToVoteData/useLockToVoteData.ts | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 .changeset/fancy-moments-cross.md diff --git a/.changeset/fancy-moments-cross.md b/.changeset/fancy-moments-cross.md new file mode 100644 index 0000000000..13612462b2 --- /dev/null +++ b/.changeset/fancy-moments-cross.md @@ -0,0 +1,5 @@ +--- +'@aragon/app': patch +--- + +Implement unique enum ID for LockToVote dialog diff --git a/src/plugins/lockToVotePlugin/constants/lockToVotePluginDialogId.ts b/src/plugins/lockToVotePlugin/constants/lockToVotePluginDialogId.ts index 81f3d32ed3..e4fc9cbe65 100644 --- a/src/plugins/lockToVotePlugin/constants/lockToVotePluginDialogId.ts +++ b/src/plugins/lockToVotePlugin/constants/lockToVotePluginDialogId.ts @@ -1,6 +1,6 @@ export enum LockToVotePluginDialogId { SUBMIT_VOTE_FEEDBACK = 'SUBMIT_VOTE_FEEDBACK', LOCK_BEFORE_VOTE = 'LOCK_BEFORE_VOTE', - LOCK_UNLOCK = 'LOCK_UNLOCK', + LOCK_UNLOCK_L2V = 'LOCK_UNLOCK_L2V', UNLOCK_BLOCKED_INFO = 'UNLOCK_BLOCKED_INFO', } diff --git a/src/plugins/lockToVotePlugin/constants/lockToVotePluginDialogsDefinitions.ts b/src/plugins/lockToVotePlugin/constants/lockToVotePluginDialogsDefinitions.ts index 7d1e52eec9..022b5576d7 100644 --- a/src/plugins/lockToVotePlugin/constants/lockToVotePluginDialogsDefinitions.ts +++ b/src/plugins/lockToVotePlugin/constants/lockToVotePluginDialogsDefinitions.ts @@ -15,7 +15,7 @@ export const lockToVotePluginDialogsDefinitions: Record refetchData(); From de9bc7751f625f56131317c6f80107d24a7e89ba Mon Sep 17 00:00:00 2001 From: "heykd.eth" <65736142+thekidnamedkd@users.noreply.github.com> Date: Wed, 17 Sep 2025 18:16:23 +0200 Subject: [PATCH 3/6] fix(APP-4537): Fix calculation for stage 'Expiration period' on process details (#763) --- .changeset/six-mugs-guess.md | 5 +++++ .../daoMemberDetailsPage/daoMemberDetailsPageClient.test.tsx | 2 +- .../daoProcessDetailsPage/daoProcessDetailsClientUtils.ts | 4 +++- 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 .changeset/six-mugs-guess.md diff --git a/.changeset/six-mugs-guess.md b/.changeset/six-mugs-guess.md new file mode 100644 index 0000000000..1a9f4c08e2 --- /dev/null +++ b/.changeset/six-mugs-guess.md @@ -0,0 +1,5 @@ +--- +'@aragon/app': patch +--- + +Fix calculation for stage 'Expiration period' on process details diff --git a/src/modules/governance/pages/daoMemberDetailsPage/daoMemberDetailsPageClient.test.tsx b/src/modules/governance/pages/daoMemberDetailsPage/daoMemberDetailsPageClient.test.tsx index 0cc0b39d88..0a83f0b87d 100644 --- a/src/modules/governance/pages/daoMemberDetailsPage/daoMemberDetailsPageClient.test.tsx +++ b/src/modules/governance/pages/daoMemberDetailsPage/daoMemberDetailsPageClient.test.tsx @@ -38,7 +38,7 @@ jest.mock('@/modules/governance/components/voteList', () => ({ VoteList: jest.fn(() =>
), })); -describe.skip(' component', () => { +describe(' component', () => { const useDaoSpy = jest.spyOn(daoService, 'useDao'); const useMemberSpy = jest.spyOn(governanceService, 'useMember'); const clipboardCopySpy = jest.spyOn(clipboardUtils, 'copy'); diff --git a/src/modules/settings/pages/daoProcessDetailsPage/daoProcessDetailsClientUtils.ts b/src/modules/settings/pages/daoProcessDetailsPage/daoProcessDetailsClientUtils.ts index 1371ccd11a..f670a4707c 100644 --- a/src/modules/settings/pages/daoProcessDetailsPage/daoProcessDetailsClientUtils.ts +++ b/src/modules/settings/pages/daoProcessDetailsPage/daoProcessDetailsClientUtils.ts @@ -102,7 +102,9 @@ export class DaoProcessDetailsClientUtils { earlyStageAdvance: stage.minAdvance === 0, requiredApprovals: stage.approvalThreshold > 0 ? stage.approvalThreshold : stage.vetoThreshold, stageExpiration: - stage.maxAdvance !== 3155760000 ? dateUtils.secondsToDuration(stage.maxAdvance) : undefined, + stage.maxAdvance !== 3155760000 + ? dateUtils.secondsToDuration(stage.maxAdvance - stage.voteDuration) + : undefined, }, bodies, }; From 19d9a090302ce176cc4983ac37aacaf9c47243e4 Mon Sep 17 00:00:00 2001 From: Milos Dzepina Date: Thu, 18 Sep 2025 10:02:02 +0200 Subject: [PATCH 4/6] fix(APP-4484): Strip headers from RPC requests to prevent 413 errors (#759) --- .changeset/nine-badgers-glow.md | 5 ++++ .../utils/proxyRpcUtils/proxyRpcUtils.test.ts | 29 +++++++++++++++---- .../utils/proxyRpcUtils/proxyRpcUtils.tsx | 15 ++++++++-- 3 files changed, 40 insertions(+), 9 deletions(-) create mode 100644 .changeset/nine-badgers-glow.md diff --git a/.changeset/nine-badgers-glow.md b/.changeset/nine-badgers-glow.md new file mode 100644 index 0000000000..866fc729aa --- /dev/null +++ b/.changeset/nine-badgers-glow.md @@ -0,0 +1,5 @@ +--- +'@aragon/app': patch +--- + +Strip all headers from RPC requests to prevent 413 errors diff --git a/src/modules/application/utils/proxyRpcUtils/proxyRpcUtils.test.ts b/src/modules/application/utils/proxyRpcUtils/proxyRpcUtils.test.ts index f1fb8aeecf..caf287cab8 100644 --- a/src/modules/application/utils/proxyRpcUtils/proxyRpcUtils.test.ts +++ b/src/modules/application/utils/proxyRpcUtils/proxyRpcUtils.test.ts @@ -115,13 +115,30 @@ describe('proxyRpc utils', () => { describe('buildRequestOptions', () => { it('returns the parameters for the fetch call', () => { const testClass = createTestClass(); - const request = generateRequest({ method: 'POST', body: {} as ReadableStream }); - expect(testClass['buildRequestOptions'](request)).toEqual({ - method: request.method, - body: request.body, - headers: request.headers, - duplex: 'half', + const request = generateRequest({ method: 'POST', body: {} as ReadableStream, credentials: 'include' }); + + const requestOptions = testClass['buildRequestOptions'](request); + + expect(requestOptions.method).toEqual(request.method); + expect(requestOptions.body).toEqual(request.body); + expect(requestOptions.duplex).toEqual('half'); + expect(requestOptions.credentials).toEqual('omit'); // always omit credentials on proxy requests + }); + + it('strips all headers from the request', () => { + const testClass = createTestClass(); + const headers = new Headers({ + 'test-header': 'test-value', + cookie: 'test-cookie=test-value', + Cookie: 'test-cookie=test-value', + }); + const request = generateRequest({ + headers, }); + + const requestOptions = testClass['buildRequestOptions'](request); + + expect(requestOptions.headers).toBeUndefined(); }); }); }); diff --git a/src/modules/application/utils/proxyRpcUtils/proxyRpcUtils.tsx b/src/modules/application/utils/proxyRpcUtils/proxyRpcUtils.tsx index 4269bf8f61..967f064771 100644 --- a/src/modules/application/utils/proxyRpcUtils/proxyRpcUtils.tsx +++ b/src/modules/application/utils/proxyRpcUtils/proxyRpcUtils.tsx @@ -53,9 +53,18 @@ export class ProxyRpcUtils { private chainIdToNetwork = (chainId: string): Network | undefined => Object.values(Network).find((network) => networkDefinitions[network as Network].id === Number(chainId)); - private buildRequestOptions = (request: Request): RequestInit => { - const { method, body, headers } = request; + // Return type extended to include Node-specific 'duplex' property used for streamed requests. + private buildRequestOptions = (request: Request): RequestInit & { duplex?: 'half' } => { + const { method, body } = request; - return { method, body, headers, duplex: 'half' } as RequestInit; + // Don't forward headers: avoid RPC 413 "Request Entity Too Large" errors caused by sending headers' data, specifically cookies. + // (Also, beneficial to prevent potential sensitive data leaks to 3rd party services.) + return { + method, + body, + // Ensure no implicit credential forwarding + credentials: 'omit', + duplex: 'half', + }; }; } From e025ee6b4a1c143fa23ed94ae9bb8dd3d0aea86b Mon Sep 17 00:00:00 2001 From: Milos Dzepina Date: Thu, 18 Sep 2025 10:41:33 +0200 Subject: [PATCH 5/6] feat(APP-4058): Implement Tenderly simulations for proposal actions (#735) --- .changeset/hot-numbers-attack.md | 5 ++ src/assets/locales/en.json | 17 +++- .../proxyBackendUtils.test.ts | 12 ++- .../proxyBackendUtils/proxyBackendUtils.ts | 12 ++- .../actionSimulationService.api.ts | 56 ++++++++++++ .../actionSimulationService.ts | 35 ++++++++ .../actionSimulationServiceKeys.ts | 9 ++ .../actionSimulationService/domain/index.ts | 1 + .../domain/simulationResult.ts | 14 +++ .../api/actionSimulationService/index.ts | 6 ++ .../mutations/index.ts | 2 + .../mutations/useSimulateActions/index.ts | 1 + .../useSimulateActions.test.ts | 27 ++++++ .../useSimulateActions/useSimulateActions.ts | 11 +++ .../mutations/useSimulateProposal/index.ts | 1 + .../useSimulateProposal.ts | 11 +++ .../actionSimulationService/queries/index.ts | 1 + .../queries/useLastSimulation/index.ts | 1 + .../useLastSimulation.test.ts | 22 +++++ .../useLastSimulation/useLastSimulation.ts | 19 +++++ .../constants/governanceDialogId.ts | 1 + .../constants/governanceDialogsDefinitions.ts | 2 + .../dialogs/simulateActionsDialog/index.ts | 6 ++ .../simulateActionsDialog.tsx | 85 +++++++++++++++++++ .../createProposalPageClient.tsx | 3 +- .../createProposalPageClientSteps.test.tsx | 15 +++- .../createProposalPageClientSteps.tsx | 44 +++++++++- .../createProposalPageDefinitions.ts | 2 + .../daoProposalDetailsPageClient.test.tsx | 6 +- .../daoProposalDetailsPageClient.tsx | 70 +++++++++++++-- .../governance/testUtils/generators/index.ts | 1 + .../testUtils/generators/simulationResult.ts | 8 ++ src/shared/components/wizards/wizard/index.ts | 3 +- .../wizardPage/wizardPageStep/index.ts | 2 +- .../wizardPageStep/wizardPageStep.tsx | 39 +++++++-- src/shared/constants/networkDefinitions.ts | 15 ++++ src/shared/types/media.d.ts | 4 - 37 files changed, 537 insertions(+), 32 deletions(-) create mode 100644 .changeset/hot-numbers-attack.md create mode 100644 src/modules/governance/api/actionSimulationService/actionSimulationService.api.ts create mode 100644 src/modules/governance/api/actionSimulationService/actionSimulationService.ts create mode 100644 src/modules/governance/api/actionSimulationService/actionSimulationServiceKeys.ts create mode 100644 src/modules/governance/api/actionSimulationService/domain/index.ts create mode 100644 src/modules/governance/api/actionSimulationService/domain/simulationResult.ts create mode 100644 src/modules/governance/api/actionSimulationService/index.ts create mode 100644 src/modules/governance/api/actionSimulationService/mutations/index.ts create mode 100644 src/modules/governance/api/actionSimulationService/mutations/useSimulateActions/index.ts create mode 100644 src/modules/governance/api/actionSimulationService/mutations/useSimulateActions/useSimulateActions.test.ts create mode 100644 src/modules/governance/api/actionSimulationService/mutations/useSimulateActions/useSimulateActions.ts create mode 100644 src/modules/governance/api/actionSimulationService/mutations/useSimulateProposal/index.ts create mode 100644 src/modules/governance/api/actionSimulationService/mutations/useSimulateProposal/useSimulateProposal.ts create mode 100644 src/modules/governance/api/actionSimulationService/queries/index.ts create mode 100644 src/modules/governance/api/actionSimulationService/queries/useLastSimulation/index.ts create mode 100644 src/modules/governance/api/actionSimulationService/queries/useLastSimulation/useLastSimulation.test.ts create mode 100644 src/modules/governance/api/actionSimulationService/queries/useLastSimulation/useLastSimulation.ts create mode 100644 src/modules/governance/dialogs/simulateActionsDialog/index.ts create mode 100644 src/modules/governance/dialogs/simulateActionsDialog/simulateActionsDialog.tsx create mode 100644 src/modules/governance/testUtils/generators/simulationResult.ts delete mode 100644 src/shared/types/media.d.ts diff --git a/.changeset/hot-numbers-attack.md b/.changeset/hot-numbers-attack.md new file mode 100644 index 0000000000..41c16178d5 --- /dev/null +++ b/.changeset/hot-numbers-attack.md @@ -0,0 +1,5 @@ +--- +'@aragon/app': minor +--- + +Implement Tenderly simulations for proposal actions diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index 8f4d0c3ca3..472f99bb89 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -748,6 +748,10 @@ } }, "createProposalPage": { + "createProposalPageClientSteps": { + "simulate": "Simulate actions", + "skipSimulation": "Skip simulation" + }, "finalStep": "Publish proposal", "steps": { "ACTIONS": { @@ -852,7 +856,9 @@ }, "main": { "actions": { - "header": "Actions" + "header": "Actions", + "lastSimulationError": "There was an error getting the last simulation data. Please try again later.", + "simulationError": "There was an error simulating the actions. Please try again later." }, "description": { "header": "Proposal", @@ -972,6 +978,15 @@ "title": "Create proposal" } }, + "simulateActionsDialog": { + "action": { + "cancel": "Cancel", + "error": "Continue anyway", + "success": "Continue" + }, + "error": "There was an error simulating the actions. Please try again later.", + "title": "Simulate actions" + }, "verifySmartContractDialog": { "action": { "add": "Add contract", diff --git a/src/modules/application/utils/proxyBackendUtils/proxyBackendUtils.test.ts b/src/modules/application/utils/proxyBackendUtils/proxyBackendUtils.test.ts index 8497c1e7a1..4d014336cd 100644 --- a/src/modules/application/utils/proxyBackendUtils/proxyBackendUtils.test.ts +++ b/src/modules/application/utils/proxyBackendUtils/proxyBackendUtils.test.ts @@ -21,12 +21,18 @@ describe('proxyBackend utils', () => { describe('request', () => { it('calls the rpc endpoint with the specified parameters, parses and returns the result', async () => { const parsedResponse = { result: 'test' }; - const fetchReturn = generateResponse({ json: jest.fn(() => Promise.resolve(parsedResponse)) }); + const headers = new Headers(); + const fetchReturn = generateResponse({ json: jest.fn(() => Promise.resolve(parsedResponse)), headers }); + const mockNextResponse = {} as NextResponse; fetchSpy.mockResolvedValue(fetchReturn); - await proxyBackendUtils.request(generateNextRequest()); + nextResponseJsonSpy.mockReturnValue(mockNextResponse); + + const result = await proxyBackendUtils.request(generateNextRequest()); + expect(fetchSpy).toHaveBeenCalled(); expect(fetchReturn.json).toHaveBeenCalled(); - expect(nextResponseJsonSpy).toHaveBeenCalledWith(parsedResponse); + expect(nextResponseJsonSpy).toHaveBeenCalledWith(parsedResponse, fetchReturn); + expect(result).toEqual(mockNextResponse); }); }); diff --git a/src/modules/application/utils/proxyBackendUtils/proxyBackendUtils.ts b/src/modules/application/utils/proxyBackendUtils/proxyBackendUtils.ts index 869fb0891a..f68adf0a33 100644 --- a/src/modules/application/utils/proxyBackendUtils/proxyBackendUtils.ts +++ b/src/modules/application/utils/proxyBackendUtils/proxyBackendUtils.ts @@ -5,11 +5,12 @@ export class ProxyBackendUtils { request = async (request: NextRequest) => { const url = this.buildBackendUrl(request); + const requestOptions = await this.buildRequestOptions(request); - const result = await fetch(url, request); + const result = await fetch(url, requestOptions); const parsedResult = (await result.json()) as unknown; - return NextResponse.json(parsedResult); + return NextResponse.json(parsedResult, result); }; private buildBackendUrl = (request: NextRequest): string => { @@ -18,6 +19,13 @@ export class ProxyBackendUtils { return url; }; + + private buildRequestOptions = async (request: NextRequest): Promise => { + const { method, headers } = request; + const body = method.toUpperCase() === 'POST' ? await request.text() : undefined; + + return { method, body, headers }; + }; } export const proxyBackendUtils = new ProxyBackendUtils(); diff --git a/src/modules/governance/api/actionSimulationService/actionSimulationService.api.ts b/src/modules/governance/api/actionSimulationService/actionSimulationService.api.ts new file mode 100644 index 0000000000..2de85150b9 --- /dev/null +++ b/src/modules/governance/api/actionSimulationService/actionSimulationService.api.ts @@ -0,0 +1,56 @@ +import type { Network } from '@/shared/api/daoService'; +import type { IRequestUrlBodyParams, IRequestUrlParams } from '@/shared/api/httpService'; + +export interface ISimulateActionsUrlParams { + /** + * Network to simulate the actions on. + */ + network: Network; + /** + * Address of the plugin to simulate the actions for. Used as `from` address. + */ + pluginAddress: string; +} + +export interface ISimulateActionsItem { + /** + * Address to simulate the transaction to. + */ + to: string; + /** + * Transaction data to simulate. + */ + data: string; + /** + * Value to send with the transaction. + */ + value: string; +} + +export interface ISimulateActionsBody { + /** + * List of actions to simulate. + */ + actions: ISimulateActionsItem[]; +} + +export interface ISimulateActionsParams + extends IRequestUrlBodyParams {} + +export interface ISimulateProposalUrlParams { + /** + * ID of the proposal to simulate. + */ + proposalId: string; +} + +export interface ISimulateProposalParams extends IRequestUrlParams {} + +export interface IGetLastSimulationUrlParams { + /** + * ID of the proposal to get last simulation for. + */ + proposalId: string; +} + +export interface IGetLastSimulationParams extends IRequestUrlParams {} diff --git a/src/modules/governance/api/actionSimulationService/actionSimulationService.ts b/src/modules/governance/api/actionSimulationService/actionSimulationService.ts new file mode 100644 index 0000000000..4c0211739c --- /dev/null +++ b/src/modules/governance/api/actionSimulationService/actionSimulationService.ts @@ -0,0 +1,35 @@ +import { AragonBackendService } from '@/shared/api/aragonBackendService'; +import type { + IGetLastSimulationParams, + ISimulateActionsParams, + ISimulateProposalParams, +} from './actionSimulationService.api'; +import type { ISimulationResult } from './domain'; + +class ActionSimulationService extends AragonBackendService { + private urls = { + simulateActions: '/v2/simulations/:network/plugin/:pluginAddress/simulate', + simulateProposal: '/v2/simulations/proposal/:proposalId', + getLastSimulation: '/v2/simulations/proposal/:proposalId', + }; + + simulateActions = async (params: ISimulateActionsParams): Promise => { + const result = await this.request(this.urls.simulateActions, params, { method: 'POST' }); + + return result; + }; + + simulateProposal = async (params: ISimulateProposalParams): Promise => { + const result = await this.request(this.urls.simulateProposal, params, { method: 'POST' }); + + return result; + }; + + getLastSimulation = async (params: IGetLastSimulationParams): Promise => { + const result = await this.request(this.urls.getLastSimulation, params); + + return result; + }; +} + +export const actionSimulationService = new ActionSimulationService(); diff --git a/src/modules/governance/api/actionSimulationService/actionSimulationServiceKeys.ts b/src/modules/governance/api/actionSimulationService/actionSimulationServiceKeys.ts new file mode 100644 index 0000000000..2b070f37e4 --- /dev/null +++ b/src/modules/governance/api/actionSimulationService/actionSimulationServiceKeys.ts @@ -0,0 +1,9 @@ +import type { IGetLastSimulationParams } from './actionSimulationService.api'; + +export enum ActionSimulationServiceKey { + LAST_SIMULATION = 'LAST_SIMULATION', +} + +export const actionSimulationServiceKeys = { + lastSimulation: (params: IGetLastSimulationParams) => [ActionSimulationServiceKey.LAST_SIMULATION, params], +}; diff --git a/src/modules/governance/api/actionSimulationService/domain/index.ts b/src/modules/governance/api/actionSimulationService/domain/index.ts new file mode 100644 index 0000000000..44e27b007f --- /dev/null +++ b/src/modules/governance/api/actionSimulationService/domain/index.ts @@ -0,0 +1 @@ +export type { ISimulationResult } from './simulationResult'; diff --git a/src/modules/governance/api/actionSimulationService/domain/simulationResult.ts b/src/modules/governance/api/actionSimulationService/domain/simulationResult.ts new file mode 100644 index 0000000000..ce1fc6dbfc --- /dev/null +++ b/src/modules/governance/api/actionSimulationService/domain/simulationResult.ts @@ -0,0 +1,14 @@ +export interface ISimulationResult { + /** + * Timestamp when the simulation was run. + */ + runAt: number; + /** + * Status of the simulation. + */ + status: 'success' | 'failed'; + /** + * URL to view the simulation in Tenderly. + */ + url: string; +} diff --git a/src/modules/governance/api/actionSimulationService/index.ts b/src/modules/governance/api/actionSimulationService/index.ts new file mode 100644 index 0000000000..3f8ca1809b --- /dev/null +++ b/src/modules/governance/api/actionSimulationService/index.ts @@ -0,0 +1,6 @@ +export { actionSimulationService } from './actionSimulationService'; +export type * from './actionSimulationService.api'; +export { ActionSimulationServiceKey, actionSimulationServiceKeys } from './actionSimulationServiceKeys'; +export type * from './domain'; +export * from './mutations'; +export * from './queries'; diff --git a/src/modules/governance/api/actionSimulationService/mutations/index.ts b/src/modules/governance/api/actionSimulationService/mutations/index.ts new file mode 100644 index 0000000000..0a0ec00c5e --- /dev/null +++ b/src/modules/governance/api/actionSimulationService/mutations/index.ts @@ -0,0 +1,2 @@ +export * from './useSimulateActions'; +export * from './useSimulateProposal'; diff --git a/src/modules/governance/api/actionSimulationService/mutations/useSimulateActions/index.ts b/src/modules/governance/api/actionSimulationService/mutations/useSimulateActions/index.ts new file mode 100644 index 0000000000..332b18bb4e --- /dev/null +++ b/src/modules/governance/api/actionSimulationService/mutations/useSimulateActions/index.ts @@ -0,0 +1 @@ +export { useSimulateActions } from './useSimulateActions'; diff --git a/src/modules/governance/api/actionSimulationService/mutations/useSimulateActions/useSimulateActions.test.ts b/src/modules/governance/api/actionSimulationService/mutations/useSimulateActions/useSimulateActions.test.ts new file mode 100644 index 0000000000..3965536fd3 --- /dev/null +++ b/src/modules/governance/api/actionSimulationService/mutations/useSimulateActions/useSimulateActions.test.ts @@ -0,0 +1,27 @@ +import { generateSimulationResult } from '@/modules/governance/testUtils'; +import { Network } from '@/shared/api/daoService'; +import { ReactQueryWrapper } from '@/shared/testUtils'; +import { act, renderHook, waitFor } from '@testing-library/react'; +import { actionSimulationService } from '../../actionSimulationService'; +import { useSimulateActions } from './useSimulateActions'; + +describe('useSimulateActions mutation', () => { + const simulateActionsSpy = jest.spyOn(actionSimulationService, 'simulateActions'); + + afterEach(() => { + simulateActionsSpy.mockReset(); + }); + + it('simulates actions and returns the result', async () => { + const simulationResult = generateSimulationResult(); + const params = { + urlParams: { network: Network.ETHEREUM_MAINNET, pluginAddress: '0x123' }, + body: { actions: [{ to: '0x456', data: '0x000', value: '0' }] }, + }; + simulateActionsSpy.mockResolvedValue(simulationResult); + const { result } = renderHook(() => useSimulateActions(), { wrapper: ReactQueryWrapper }); + act(() => result.current.mutate(params)); + await waitFor(() => expect(result.current.data).toEqual(simulationResult)); + expect(simulateActionsSpy).toHaveBeenCalledWith(params); + }); +}); diff --git a/src/modules/governance/api/actionSimulationService/mutations/useSimulateActions/useSimulateActions.ts b/src/modules/governance/api/actionSimulationService/mutations/useSimulateActions/useSimulateActions.ts new file mode 100644 index 0000000000..6eb3aaa094 --- /dev/null +++ b/src/modules/governance/api/actionSimulationService/mutations/useSimulateActions/useSimulateActions.ts @@ -0,0 +1,11 @@ +import { useMutation, type MutationOptions } from '@tanstack/react-query'; +import { actionSimulationService } from '../../actionSimulationService'; +import type { ISimulateActionsParams } from '../../actionSimulationService.api'; +import type { ISimulationResult } from '../../domain'; + +export const useSimulateActions = (options?: MutationOptions) => { + return useMutation({ + mutationFn: (params) => actionSimulationService.simulateActions(params), + ...options, + }); +}; diff --git a/src/modules/governance/api/actionSimulationService/mutations/useSimulateProposal/index.ts b/src/modules/governance/api/actionSimulationService/mutations/useSimulateProposal/index.ts new file mode 100644 index 0000000000..2dba000a6e --- /dev/null +++ b/src/modules/governance/api/actionSimulationService/mutations/useSimulateProposal/index.ts @@ -0,0 +1 @@ +export { useSimulateProposal } from './useSimulateProposal'; diff --git a/src/modules/governance/api/actionSimulationService/mutations/useSimulateProposal/useSimulateProposal.ts b/src/modules/governance/api/actionSimulationService/mutations/useSimulateProposal/useSimulateProposal.ts new file mode 100644 index 0000000000..fec7a78be2 --- /dev/null +++ b/src/modules/governance/api/actionSimulationService/mutations/useSimulateProposal/useSimulateProposal.ts @@ -0,0 +1,11 @@ +import { useMutation, type MutationOptions } from '@tanstack/react-query'; +import { actionSimulationService } from '../../actionSimulationService'; +import type { ISimulateProposalParams } from '../../actionSimulationService.api'; +import type { ISimulationResult } from '../../domain'; + +export const useSimulateProposal = (options?: MutationOptions) => { + return useMutation({ + mutationFn: (params) => actionSimulationService.simulateProposal(params), + ...options, + }); +}; diff --git a/src/modules/governance/api/actionSimulationService/queries/index.ts b/src/modules/governance/api/actionSimulationService/queries/index.ts new file mode 100644 index 0000000000..81b6e9d343 --- /dev/null +++ b/src/modules/governance/api/actionSimulationService/queries/index.ts @@ -0,0 +1 @@ +export * from './useLastSimulation'; diff --git a/src/modules/governance/api/actionSimulationService/queries/useLastSimulation/index.ts b/src/modules/governance/api/actionSimulationService/queries/useLastSimulation/index.ts new file mode 100644 index 0000000000..a94560519b --- /dev/null +++ b/src/modules/governance/api/actionSimulationService/queries/useLastSimulation/index.ts @@ -0,0 +1 @@ +export { lastSimulationOptions, useLastSimulation } from './useLastSimulation'; diff --git a/src/modules/governance/api/actionSimulationService/queries/useLastSimulation/useLastSimulation.test.ts b/src/modules/governance/api/actionSimulationService/queries/useLastSimulation/useLastSimulation.test.ts new file mode 100644 index 0000000000..b708b95f06 --- /dev/null +++ b/src/modules/governance/api/actionSimulationService/queries/useLastSimulation/useLastSimulation.test.ts @@ -0,0 +1,22 @@ +import { generateSimulationResult } from '@/modules/governance/testUtils'; +import { ReactQueryWrapper } from '@/shared/testUtils'; +import { renderHook, waitFor } from '@testing-library/react'; +import { actionSimulationService } from '../../actionSimulationService'; +import { useLastSimulation } from './useLastSimulation'; + +describe('useLastSimulation query', () => { + const getLastSimulationSpy = jest.spyOn(actionSimulationService, 'getLastSimulation'); + + afterEach(() => { + getLastSimulationSpy.mockReset(); + }); + + it('fetches last simulation result for a proposal', async () => { + const simulationResult = generateSimulationResult(); + const params = { urlParams: { proposalId: 'proposal-123' } }; + getLastSimulationSpy.mockResolvedValue(simulationResult); + const { result } = renderHook(() => useLastSimulation(params), { wrapper: ReactQueryWrapper }); + await waitFor(() => expect(result.current.data).toEqual(simulationResult)); + expect(getLastSimulationSpy).toHaveBeenCalledWith(params); + }); +}); diff --git a/src/modules/governance/api/actionSimulationService/queries/useLastSimulation/useLastSimulation.ts b/src/modules/governance/api/actionSimulationService/queries/useLastSimulation/useLastSimulation.ts new file mode 100644 index 0000000000..73403b9221 --- /dev/null +++ b/src/modules/governance/api/actionSimulationService/queries/useLastSimulation/useLastSimulation.ts @@ -0,0 +1,19 @@ +import type { QueryOptions, SharedQueryOptions } from '@/shared/types'; +import { useQuery } from '@tanstack/react-query'; +import { actionSimulationService } from '../../actionSimulationService'; +import type { IGetLastSimulationParams } from '../../actionSimulationService.api'; +import { actionSimulationServiceKeys } from '../../actionSimulationServiceKeys'; +import type { ISimulationResult } from '../../domain'; + +export const lastSimulationOptions = ( + params: IGetLastSimulationParams, + options?: QueryOptions, +): SharedQueryOptions => ({ + queryKey: actionSimulationServiceKeys.lastSimulation(params), + queryFn: () => actionSimulationService.getLastSimulation(params), + ...options, +}); + +export const useLastSimulation = (params: IGetLastSimulationParams, options?: QueryOptions) => { + return useQuery(lastSimulationOptions(params, options)); +}; diff --git a/src/modules/governance/constants/governanceDialogId.ts b/src/modules/governance/constants/governanceDialogId.ts index ecad789dcb..69ac79fae6 100644 --- a/src/modules/governance/constants/governanceDialogId.ts +++ b/src/modules/governance/constants/governanceDialogId.ts @@ -6,4 +6,5 @@ export enum GovernanceDialogId { VERIFY_SMART_CONTRACT = 'VERIFY_SMART_CONTRACT', PERMISSION_CHECK = 'PERMISSION_CHECK', WALLET_CONNECT_ACTION = 'WALLET_CONNECT_ACTION', + SIMULATE_ACTIONS = 'SIMULATE_ACTIONS', } diff --git a/src/modules/governance/constants/governanceDialogsDefinitions.ts b/src/modules/governance/constants/governanceDialogsDefinitions.ts index 0c69d71d3b..af5e077863 100644 --- a/src/modules/governance/constants/governanceDialogsDefinitions.ts +++ b/src/modules/governance/constants/governanceDialogsDefinitions.ts @@ -3,6 +3,7 @@ import { ExecuteDialog } from '../dialogs/executeDialog'; import { PermissionCheckDialog } from '../dialogs/permissionCheckDialog'; import { PublishProposalDialog } from '../dialogs/publishProposalDialog'; import { SelectPluginDialog } from '../dialogs/selectPluginDialog'; +import { SimulateActionsDialog } from '../dialogs/simulateActionsDialog'; import { VerifySmartContractDialog } from '../dialogs/verifySmartContractDialog'; import { VoteDialog } from '../dialogs/voteDialog'; import { WalletConnectActionDialog } from '../dialogs/walletConnectActionDialog'; @@ -19,4 +20,5 @@ export const governanceDialogsDefinitions: Record + import('./simulateActionsDialog').then((mod) => mod.SimulateActionsDialog), +); +export type { ISimulateActionsDialogParams, ISimulateActionsDialogProps } from './simulateActionsDialog'; diff --git a/src/modules/governance/dialogs/simulateActionsDialog/simulateActionsDialog.tsx b/src/modules/governance/dialogs/simulateActionsDialog/simulateActionsDialog.tsx new file mode 100644 index 0000000000..856f85a331 --- /dev/null +++ b/src/modules/governance/dialogs/simulateActionsDialog/simulateActionsDialog.tsx @@ -0,0 +1,85 @@ +import type { Network } from '@/shared/api/daoService'; +import { useDialogContext, type IDialogComponentProps } from '@/shared/components/dialogProvider'; +import { useTranslations } from '@/shared/components/translationsProvider'; +import { ActionSimulation, Dialog, invariant } from '@aragon/gov-ui-kit'; +import { useEffect } from 'react'; +import { useSimulateActions } from '../../api/actionSimulationService'; +import type { IProposalCreateAction } from '../publishProposalDialog'; + +export interface ISimulateActionsDialogParams { + /** + * Network of the DAO. + */ + network: Network; + /** + * Address of the plugin on which the proposal is created. + */ + pluginAddress: string; + /** + * List of actions to simulate. + */ + actions: IProposalCreateAction[]; + /** + * ID of the form to trigger the submit for. + */ + formId?: string; +} + +export interface ISimulateActionsDialogProps extends IDialogComponentProps {} + +export const SimulateActionsDialog: React.FC = (props) => { + const { location } = props; + + invariant(location.params != null, 'SimulateActionsDialog: params must be set for the dialog to work correctly'); + const { actions, network, pluginAddress, formId } = location.params; + + const { t } = useTranslations(); + const { close } = useDialogContext(); + + const { mutate: simulateActions, isError, isPending, status, data } = useSimulateActions(); + + useEffect(() => { + if (status !== 'idle') { + return; + } + + const urlParams = { network, pluginAddress }; + const processedActions = actions.map(({ to, data, value }) => ({ to, data, value: value.toString() })); + simulateActions({ urlParams, body: { actions: processedActions } }); + }, [actions, network, pluginAddress, status, simulateActions]); + + const hasSimulationFailed = isError || data?.status === 'failed'; + const lastSimulation = data != null ? { ...data, timestamp: data.runAt } : undefined; + + const error = isError ? t('app.governance.simulateActionsDialog.error') : undefined; + const primaryLabel = t(`app.governance.simulateActionsDialog.action.${hasSimulationFailed ? 'error' : 'success'}`); + + return ( + <> + + + + + close(), + }} + secondaryAction={{ + label: t('app.governance.simulateActionsDialog.action.cancel'), + onClick: () => close(), + }} + /> + + ); +}; diff --git a/src/modules/governance/pages/createProposalPage/createProposalPageClient.tsx b/src/modules/governance/pages/createProposalPage/createProposalPageClient.tsx index 8205f51193..aedc63f947 100644 --- a/src/modules/governance/pages/createProposalPage/createProposalPageClient.tsx +++ b/src/modules/governance/pages/createProposalPage/createProposalPageClient.tsx @@ -15,7 +15,7 @@ import type { } from '../../dialogs/publishProposalDialog'; import { useProposalPermissionCheckGuard } from '../../hooks/useProposalPermissionCheckGuard'; import { CreateProposalPageClientSteps } from './createProposalPageClientSteps'; -import { createProposalWizardSteps } from './createProposalPageDefinitions'; +import { createProposalWizardId, createProposalWizardSteps } from './createProposalPageDefinitions'; export interface ICreateProposalPageClientProps { /** @@ -73,6 +73,7 @@ export const CreateProposalPageClient: React.FC initialSteps={processedSteps} onSubmit={handleFormSubmit} defaultValues={{ actions: [] }} + id={createProposalWizardId} > diff --git a/src/modules/governance/pages/createProposalPage/createProposalPageClientSteps.test.tsx b/src/modules/governance/pages/createProposalPage/createProposalPageClientSteps.test.tsx index 73952b02bb..46b02c0899 100644 --- a/src/modules/governance/pages/createProposalPage/createProposalPageClientSteps.test.tsx +++ b/src/modules/governance/pages/createProposalPage/createProposalPageClientSteps.test.tsx @@ -1,9 +1,11 @@ +import * as DialogProvider from '@/shared/components/dialogProvider'; import type { IWizardPageStepProps } from '@/shared/components/wizards/wizardPage'; import * as useDaoPlugins from '@/shared/hooks/useDaoPlugins'; -import { generateTabComponentPlugin } from '@/shared/testUtils'; +import { generateDialogContext, generateTabComponentPlugin } from '@/shared/testUtils'; import { pluginRegistryUtils } from '@/shared/utils/pluginRegistryUtils'; import { render, screen } from '@testing-library/react'; import * as ReactHookForm from 'react-hook-form'; +import * as CreateProposalProvider from '../../components/createProposalForm/createProposalFormProvider'; import { CreateProposalPageClientSteps, type ICreateProposalPageClientStepsProps, @@ -32,22 +34,31 @@ describe(' component', () => { const useWatchSpy: jest.SpyInstance = jest.spyOn(ReactHookForm, 'useWatch'); const useDaoPluginsSpy = jest.spyOn(useDaoPlugins, 'useDaoPlugins'); const getSlotComponentSpy = jest.spyOn(pluginRegistryUtils, 'getSlotComponent'); + const useDialogContextSpy = jest.spyOn(DialogProvider, 'useDialogContext'); + const useCreateProposalFormContextSpy = jest.spyOn(CreateProposalProvider, 'useCreateProposalFormContext'); beforeEach(() => { useWatchSpy.mockReturnValue(true); useDaoPluginsSpy.mockReturnValue([generateTabComponentPlugin()]); getSlotComponentSpy.mockReturnValue(undefined); + useDialogContextSpy.mockReturnValue(generateDialogContext()); + useCreateProposalFormContextSpy.mockReturnValue({ + prepareActions: {}, + addPrepareAction: jest.fn(), + }); }); afterEach(() => { useWatchSpy.mockReset(); useDaoPluginsSpy.mockReset(); getSlotComponentSpy.mockReset(); + useDialogContextSpy.mockReset(); + useCreateProposalFormContextSpy.mockReset(); }); const createTestComponent = (props?: Partial) => { const completeProps: ICreateProposalPageClientStepsProps = { - daoId: 'test', + daoId: 'ethereum-mainnet-0x123', pluginAddress: '0x123', steps: createProposalWizardSteps, ...props, diff --git a/src/modules/governance/pages/createProposalPage/createProposalPageClientSteps.tsx b/src/modules/governance/pages/createProposalPage/createProposalPageClientSteps.tsx index 425c91e96b..744a061c02 100644 --- a/src/modules/governance/pages/createProposalPage/createProposalPageClientSteps.tsx +++ b/src/modules/governance/pages/createProposalPage/createProposalPageClientSteps.tsx @@ -1,14 +1,21 @@ 'use client'; +import { useDialogContext } from '@/shared/components/dialogProvider'; import { useTranslations } from '@/shared/components/translationsProvider'; -import type { IWizardStepperStep } from '@/shared/components/wizards/wizard'; +import { type IWizardStepperStep } from '@/shared/components/wizards/wizard'; import { WizardPage } from '@/shared/components/wizards/wizardPage'; +import { networkDefinitions } from '@/shared/constants/networkDefinitions'; import { useDaoPlugins } from '@/shared/hooks/useDaoPlugins'; +import { daoUtils } from '@/shared/utils/daoUtils'; import { pluginRegistryUtils } from '@/shared/utils/pluginRegistryUtils'; import { useWatch } from 'react-hook-form'; import { CreateProposalForm, type ICreateProposalFormData } from '../../components/createProposalForm'; +import { useCreateProposalFormContext } from '../../components/createProposalForm/createProposalFormProvider'; +import { GovernanceDialogId } from '../../constants/governanceDialogId'; import { GovernanceSlotId } from '../../constants/moduleSlots'; -import { CreateProposalWizardStep } from './createProposalPageDefinitions'; +import { publishProposalDialogUtils } from '../../dialogs/publishProposalDialog/publishProposalDialogUtils'; +import type { ISimulateActionsDialogParams } from '../../dialogs/simulateActionsDialog'; +import { createProposalWizardId, CreateProposalWizardStep } from './createProposalPageDefinitions'; export interface ICreateProposalPageClientStepsProps { /** @@ -29,7 +36,11 @@ export const CreateProposalPageClientSteps: React.FC({ name: 'addActions' }); + const actions = useWatch>({ name: 'actions' }); + const { prepareActions } = useCreateProposalFormContext(); const [metadataStep, actionsStep, settingsStep] = steps; @@ -38,6 +49,34 @@ export const CreateProposalPageClientSteps: React.FC { + const processedActions = await publishProposalDialogUtils.prepareActions({ actions, prepareActions }); + + const { network } = daoUtils.parseDaoId(daoId); + + const params: ISimulateActionsDialogParams = { + network, + pluginAddress, + actions: processedActions, + formId: createProposalWizardId, + }; + open(GovernanceDialogId.SIMULATE_ACTIONS, { params }); + }; + + const getActionStepDropdownItems = () => { + const labelBase = 'app.governance.createProposalPage.createProposalPageClientSteps'; + + const { network } = daoUtils.parseDaoId(daoId); + const { tenderlySupport } = networkDefinitions[network]; + + const dropdownItems = [ + { label: t(`${labelBase}.simulate`), onClick: handleSimulateActions }, + { label: t(`${labelBase}.skipSimulation`), formId: createProposalWizardId }, + ]; + + return actions.length > 0 && tenderlySupport ? dropdownItems : undefined; + }; + return ( <>
{submitHelpText && (

diff --git a/src/shared/constants/networkDefinitions.ts b/src/shared/constants/networkDefinitions.ts index a7ecc4336e..5d8868ab50 100644 --- a/src/shared/constants/networkDefinitions.ts +++ b/src/shared/constants/networkDefinitions.ts @@ -71,6 +71,10 @@ export interface INetworkDefinition extends Chain { * Whether the network is disabled in DAO creation. */ disabled?: boolean; + /** + * Wheter the network is supported by Tenderly. + */ + tenderlySupport: boolean; } const latestProtocolVersion: IContractVersionInfo = { @@ -91,6 +95,7 @@ export const networkDefinitions: Record = { privateRpc: 'https://eth-mainnet.g.alchemy.com/v2/', order: 1, protocolVersion: latestProtocolVersion, + tenderlySupport: true, addresses: { dao: '0x58C1F7Bc62Bb63fb137bc8F6d8ea6321a0501d29', daoFactory: '0x246503df057A9a85E0144b6867a828c99676128B', @@ -106,6 +111,7 @@ export const networkDefinitions: Record = { privateRpc: 'https://polygon-mainnet.g.alchemy.com/v2/', order: 2, protocolVersion: latestProtocolVersion, + tenderlySupport: true, addresses: { dao: '0xDC5E714720797Fa0B453Bc9eF5049548C79031C3', daoFactory: '0x9BC7f1dc3cFAD56a0EcD924D1f9e70f5C7aF0039', @@ -121,6 +127,7 @@ export const networkDefinitions: Record = { privateRpc: 'https://base-mainnet.g.alchemy.com/v2/', order: 3, protocolVersion: latestProtocolVersion, + tenderlySupport: true, addresses: { dao: '0xBeb2271224D22BdA388B513268873387E5BfC27f', daoFactory: '0xcc602EA573a42eBeC290f33F49D4A87177ebB8d2', @@ -136,6 +143,7 @@ export const networkDefinitions: Record = { privateRpc: 'https://arb-mainnet.g.alchemy.com/v2/', order: 4, protocolVersion: latestProtocolVersion, + tenderlySupport: true, addresses: { dao: '0xc3F1f4d3B4E24b6F019120205e12A01D733BEb55', daoFactory: '0x49e04AB7af7A263b8ac802c1cAe22f5b4E4577Cd', @@ -151,6 +159,7 @@ export const networkDefinitions: Record = { privateRpc: 'https://opt-mainnet.g.alchemy.com/v2/', order: 5, protocolVersion: latestProtocolVersion, + tenderlySupport: true, addresses: { dao: '0x42D24803D8697050CA59f6E306322eC9fce8D7e9', daoFactory: '0xB001Bd6A21056c2a7FB5A5b9005cf896b181e74d', @@ -166,6 +175,7 @@ export const networkDefinitions: Record = { privateRpc: 'https://zksync-mainnet.g.alchemy.com/v2/', order: 6, protocolVersion: latestProtocolVersion, + tenderlySupport: true, addresses: { dao: '0x9B43625b28fa32CaB68d84F1B46E2721DD70Ba42', daoFactory: '0x01019505E3B87340d7Fa69EF3E2510A7642f067A', @@ -181,6 +191,7 @@ export const networkDefinitions: Record = { order: 7, protocolVersion: latestProtocolVersion, beta: true, + tenderlySupport: true, addresses: { dao: '0x604953e159562FeEfF38961541415B0C0694Ef5A', daoFactory: '0x72f635574C797Bab5eB82489Aa906cE23d9aAD6f', @@ -196,6 +207,7 @@ export const networkDefinitions: Record = { order: 8, protocolVersion: latestProtocolVersion, beta: true, + tenderlySupport: false, addresses: { dao: '0x221B2d4fF2dEf7Bb1Da68460760B299e4c2D8AdD', daoFactory: '0xdD68D6b46b887AcB795eCC3Fc7bb3fEf2Dfebf8f', @@ -211,6 +223,7 @@ export const networkDefinitions: Record = { order: 9, protocolVersion: latestProtocolVersion, beta: true, + tenderlySupport: true, addresses: { dao: '0xa8a4Dc9B6f16BEe4E527CEA47FBeb6e0802030e1', daoFactory: '0x35B62715459cB60bf6dC17fF8cfe138EA305E7Ee', @@ -228,6 +241,7 @@ export const networkDefinitions: Record = { privateRpc: 'https://eth-sepolia.g.alchemy.com/v2/', order: 0, protocolVersion: latestProtocolVersion, + tenderlySupport: true, addresses: { dao: '0x824d4AAD1cbF2327c4C429E3c97F968Ee19344F8', daoFactory: '0xB815791c233807D39b7430127975244B36C19C8e', @@ -243,6 +257,7 @@ export const networkDefinitions: Record = { privateRpc: 'https://zksync-sepolia.g.alchemy.com/v2/', order: 10, protocolVersion: latestProtocolVersion, + tenderlySupport: true, addresses: { dao: '0x39e836A6c32163733929B213965e3feC0007914a', daoFactory: '0xee321f16f7F0a0F0d8b850E70c4eAde4A288ECd7', diff --git a/src/shared/types/media.d.ts b/src/shared/types/media.d.ts deleted file mode 100644 index b688c1e205..0000000000 --- a/src/shared/types/media.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '*.mp4' { - const src: string; - export default src; -} From 9820915e6a0d7bc4e3081e4d11b80e07c6eec760 Mon Sep 17 00:00:00 2001 From: "heykd.eth" <65736142+thekidnamedkd@users.noreply.github.com> Date: Fri, 19 Sep 2025 16:07:14 +0200 Subject: [PATCH 6/6] fix(APP-4472): Update isSupportReached BigInt eval for token-based plugin utils (#769) --- .changeset/two-sloths-look.md | 5 +++++ .../lockToVoteProposalUtils/lockToVoteProposalUtils.ts | 8 ++++++-- .../utils/tokenProposalUtils/tokenProposalUtils.ts | 7 ++++--- .../utils/tokenSettingsUtils/tokenSettingsUtils.tsx | 6 ++++++ 4 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 .changeset/two-sloths-look.md diff --git a/.changeset/two-sloths-look.md b/.changeset/two-sloths-look.md new file mode 100644 index 0000000000..20ee1b8796 --- /dev/null +++ b/.changeset/two-sloths-look.md @@ -0,0 +1,5 @@ +--- +'@aragon/app': patch +--- + +Fix isSupportReached utility for token-based plugins to handle support-threshold setting with decimals diff --git a/src/plugins/lockToVotePlugin/utils/lockToVoteProposalUtils/lockToVoteProposalUtils.ts b/src/plugins/lockToVotePlugin/utils/lockToVoteProposalUtils/lockToVoteProposalUtils.ts index 0faaa19338..8b0aba3d19 100644 --- a/src/plugins/lockToVotePlugin/utils/lockToVoteProposalUtils/lockToVoteProposalUtils.ts +++ b/src/plugins/lockToVotePlugin/utils/lockToVoteProposalUtils/lockToVoteProposalUtils.ts @@ -69,12 +69,16 @@ class LockToVoteProposalUtils { const { supportThreshold } = proposal.settings; const { votesByOption } = proposal.metrics; - const parsedSupport = BigInt(tokenSettingsUtils.ratioToPercentage(supportThreshold)); const yesVotes = this.getVoteByType(votesByOption, VoteOption.YES); const noVotesCurrent = this.getVoteByType(votesByOption, VoteOption.NO); + + // Keeps mental model more closely aligned with token plugin implementation const noVotesComparator = noVotesCurrent; - return (BigInt(100) - parsedSupport) * yesVotes > parsedSupport * noVotesComparator; + return ( + (tokenSettingsUtils.ratioBase - BigInt(supportThreshold)) * yesVotes > + BigInt(supportThreshold) * noVotesComparator + ); }; getTotalVotes = (proposal: ILockToVoteProposal, excludeAbstain?: boolean): bigint => { diff --git a/src/plugins/tokenPlugin/utils/tokenProposalUtils/tokenProposalUtils.ts b/src/plugins/tokenPlugin/utils/tokenProposalUtils/tokenProposalUtils.ts index d78ea0c380..ff0baa4ca0 100644 --- a/src/plugins/tokenPlugin/utils/tokenProposalUtils/tokenProposalUtils.ts +++ b/src/plugins/tokenPlugin/utils/tokenProposalUtils/tokenProposalUtils.ts @@ -77,8 +77,6 @@ class TokenProposalUtils { const { supportThreshold, historicalTotalSupply } = proposal.settings; const { votesByOption } = proposal.metrics; - const parsedSupport = BigInt(tokenSettingsUtils.ratioToPercentage(supportThreshold)); - const yesVotes = this.getVoteByType(votesByOption, VoteOption.YES); const abstainVotes = this.getVoteByType(votesByOption, VoteOption.ABSTAIN); @@ -88,7 +86,10 @@ class TokenProposalUtils { // For early-execution, check that the support threshold is met even if all remaining votes are no votes. const noVotesComparator = early ? noVotesWorstCase : noVotesCurrent; - return (BigInt(100) - parsedSupport) * yesVotes > parsedSupport * noVotesComparator; + return ( + (tokenSettingsUtils.ratioBase - BigInt(supportThreshold)) * yesVotes > + BigInt(supportThreshold) * noVotesComparator + ); }; getTotalVotes = (proposal: ITokenProposal, excludeAbstain?: boolean): bigint => { diff --git a/src/plugins/tokenPlugin/utils/tokenSettingsUtils/tokenSettingsUtils.tsx b/src/plugins/tokenPlugin/utils/tokenSettingsUtils/tokenSettingsUtils.tsx index 5baee0f465..d3c7081856 100644 --- a/src/plugins/tokenPlugin/utils/tokenSettingsUtils/tokenSettingsUtils.tsx +++ b/src/plugins/tokenPlugin/utils/tokenSettingsUtils/tokenSettingsUtils.tsx @@ -20,6 +20,12 @@ export interface IParseTokenSettingsParams { } class TokenSettingsUtils { + /** + * The base ratio used in Aragon governance contract calculations (10^6) + * Defined as RATIO_BASE in Aragon OSX contracts. + */ + readonly ratioBase = BigInt(1_000_000); + /** * Percentage values for token-based plugin settings are stored values between 0 and 10**6 (defined as RATIO_BASE). * The function parses the value set on the blockchain and returns it as percentage value between 0 and 100.