Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .agents/shared/metrics/hits.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,4 @@
{"ts":"2026-08-04T12:34:00.962Z","tool":"Edit","file":"apps/app/src/modules/governance/api/executeSelectorsService/queries/useAllAllowedActions/useAllAllowedActions.ts","rule":"query-and-cache","bytes":2385,"elapsed_ms":3,"adapter":"claude"}
{"ts":"2026-08-04T12:34:05.191Z","tool":"Edit","file":"apps/app/src/modules/governance/api/executeSelectorsService/queries/useAllAllowedActions/useAllAllowedActions.ts","rule":"query-and-cache","bytes":2385,"elapsed_ms":1,"adapter":"claude"}
{"ts":"2026-08-04T12:34:06.974Z","tool":"Edit","file":"apps/app/src/modules/governance/api/executeSelectorsService/queries/useAllAllowedActions/useAllAllowedActions.ts","rule":"query-and-cache","bytes":2385,"elapsed_ms":7,"adapter":"claude"}
{"ts":"2026-08-05T14:35:43.784Z","tool":"Write","file":"apps/app/src/plugins/crossChainControllerPlugin/api/crossChainControllerService/domain/gasLimitEstimation.ts","rule":"query-and-cache","bytes":2385,"elapsed_ms":9,"adapter":"claude"}
13 changes: 13 additions & 0 deletions apps/app/src/assets/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2541,6 +2541,19 @@
"defaultToken": "fee tokens",
"description": "The cross-chain controller ({{address}}) pays the fee to send the message. Make sure it holds enough {{token}}, otherwise the proposal execution will fail.",
"title": "Cross-chain fees are paid by the controller"
},
"gas": {
"calculate": "Calculate",
"error": "The gas limit could not be calculated. Try again, or enter a value manually if the problem persists.",
"exceedsMax": "The actions need more gas than a single cross-chain message allows ({{maxGasLimit}}). Split them across several forward actions.",
"helpText": "The gas the destination chain may spend running the actions. Calculate it by simulating the actions, or enter it manually. It is fixed when the proposal is created and cannot be changed later, so it is cleared whenever the destination or the actions change.",
"label": "Destination gas limit",
"marginReduced": "Simulated successfully. The delivery needs {{requiredGas}} gas, but the lane does not allow the usual safety margin on top, so the limit was set to the maximum the lane accepts.",
"placeholder": "Calculate or enter a gas limit",
"reverted": "The actions failed when simulated on the destination chain, so no gas limit could be measured: {{reason}}",
"simulated": "Simulated successfully. The delivery needs {{requiredGas}} gas, and a {{bufferPercent}}% safety margin was added on top.",
"unknownReason": "no reason given",
"viewSimulation": "View simulation"
}
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { Network } from '@/shared/api/daoService';
import type { IRequestUrlBodyParams } from '@/shared/api/httpService';

export interface IEstimateGasLimitUrlParams {
/**
* Network of the DAO, i.e. the origin chain the message is forwarded from.
*/
network: Network;
/**
* Address of the cross-chain controller on the origin chain. The backend reads its
* `chainToAdapter` config to resolve the lane, so the adapters are never taken from the client.
*/
controllerAddress: string;
}

export interface IEstimateGasLimitActionItem {
/**
* Address the action calls on the destination chain.
*/
to: string;
/**
* Value the action sends, as a decimal string.
*/
value: string;
/**
* Calldata of the action.
*/
data: string;
}

export interface IEstimateGasLimitBody {
/**
* Standard chain id the message is forwarded to.
*/
destinationChainId: number;
/**
* Actions the destination executor runs as a single batch.
*/
actions: IEstimateGasLimitActionItem[];
}

export interface IEstimateGasLimitParams
extends IRequestUrlBodyParams<
IEstimateGasLimitUrlParams,
IEstimateGasLimitBody
> {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { AragonBackendService } from '@/shared/api/aragonBackendService';
import type { IEstimateGasLimitParams } from './crossChainControllerService.api';
import type { IGasLimitEstimation } from './domain';

class CrossChainControllerService extends AragonBackendService {
private urls = {
estimateGasLimit:
'/v2/simulations/:network/cross-chain/:controllerAddress/gas-limit',
};

/**
* Simulates the inbound delivery on the destination chain and returns the `_gasLimit` the
* forwarded message needs.
*
* Runs on the backend because the answer cannot be obtained from `eth_estimateGas`: the
* controller wraps the payload in a `try/catch`, so the node's binary search settles on the
* cost of the catch branch and never measures the actions at all.
*/
estimateGasLimit = async (
params: IEstimateGasLimitParams,
): Promise<IGasLimitEstimation> => {
const result = await this.request<IGasLimitEstimation>(
this.urls.estimateGasLimit,
params,
{ method: 'POST' },
);

return result;
};
}

export const crossChainControllerService = new CrossChainControllerService();
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
export enum GasLimitEstimationStatus {
/**
* The simulated delivery executed the actions on the destination chain.
*/
SUCCESS = 'success',
/**
* The actions were reached but reverted, so no gas figure could be measured.
*/
REVERTED = 'reverted',
}

export interface IGasLimitEstimation {
/**
* Outcome of the simulation. Only `success` carries a usable `requiredGas`.
*/
status: GasLimitEstimationStatus;
/**
* Gas the delivery consumed in simulation, including the reserve the controller withholds from
* the payload, as a decimal string. Set only when `status` is `success`.
*
* This is a measurement, not a recommendation: it carries no safety margin and is not checked
* against the lane's per-message gas cap. Applying a margin and deciding whether it fits are
* the client's, via `crossChainControllerGasUtils`.
*/
requiredGas?: string;
/**
* Decoded revert reason of the failing action. Set when `status` is `reverted`.
*/
revertReason?: string;
/**
* Zero-based index of the action that reverted, when the backend can attribute it.
*/
revertedActionIndex?: number;
/**
* URL of the saved Tenderly simulation, for the user to inspect the trace.
*/
simulationUrl?: string;
/**
* Timestamp of the simulation, in milliseconds.
*/
runAt: number;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './gasLimitEstimation';
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { crossChainControllerService } from './crossChainControllerService';
export type * from './crossChainControllerService.api';
export * from './domain';
export * from './mutations';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useEstimateGasLimit';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useEstimateGasLimit } from './useEstimateGasLimit';
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { type MutationOptions, useMutation } from '@tanstack/react-query';
import { crossChainControllerService } from '../../crossChainControllerService';
import type { IEstimateGasLimitParams } from '../../crossChainControllerService.api';
import type { IGasLimitEstimation } from '../../domain';

export const useEstimateGasLimit = (
options?: MutationOptions<
IGasLimitEstimation,
unknown,
IEstimateGasLimitParams
>,
) =>
useMutation({
mutationFn: (params) =>
crossChainControllerService.estimateGasLimit(params),
...options,
});
Loading
Loading