@morpho-org/evm-simulation 5.0.1 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -46,7 +46,7 @@ try {
46
46
  }
47
47
  ```
48
48
 
49
- Every chain entry requires `simulateV1Url`, pointing to a JSON-RPC endpoint that supports `eth_simulateV1`. Requests send `validation: false` so gas is not charged. Monad (chain 143) is handled internally: its nodes reject `false`, so it sends `true` (its simulated block charges no gas either way), and it pins to the `finalized` block by default because its `latest` block is not final. On Monad, `gasUsed` reports the call's gas limit, not the gas consumed. Execution uses the full `timeoutMs` budget (default 5000 ms), with no retries or provider fallback. RPC failures, timeouts and reverts throw typed errors. The optional logger still reports parsing and retention warnings.
49
+ Every chain entry requires `simulateV1Url`, pointing to a JSON-RPC endpoint that supports `eth_simulateV1`. Requests send `validation: false` so gas is not charged. Monad (chain 143) is handled internally: its nodes reject `false`, so it sends `true` (its simulated block charges no gas either way), and it pins to the `finalized` block by default because its `latest` block is not final. On Monad, `gasUsed` reports the call's gas limit, not the gas consumed. Set `blockOverrides.gasLimit` on a chain entry to send that gas limit as the simulated block's `blockOverrides.gasLimit`; without it, no block override is sent. Set `parentHashCheck` on a chain entry to turn on or off the check that the simulated block's `parentHash` is the pinned block hash. It defaults to off on Stable (chain 988), whose nodes report a `parentHash` that never matches the pinned block, and on for every other chain. The block number, timestamp and reorg checks always apply. Execution uses the full `timeoutMs` budget (default 5000 ms), with no retries or provider fallback. RPC failures, timeouts and reverts throw typed errors. The optional logger still reports parsing and retention warnings.
50
50
 
51
51
  Native-ETH movements are observed through `traceTransfers` logs on the simulated calls — no `stateOverrides` or helper contracts are injected.
52
52
 
@@ -29,7 +29,9 @@ export declare function assertEndpointChain(params: {
29
29
  * synthesizes native-ETH moves as transfer logs, and `validation: false`
30
30
  * (`true` on Monad, whose nodes reject `false` and charge no gas in the
31
31
  * simulated block). Validation-off means gas is not charged, which is how
32
- * gas is separated from economic effects.
32
+ * gas is separated from economic effects. When `blockGasLimit` is set,
33
+ * it is sent as the block's `blockOverrides.gasLimit`; otherwise no block
34
+ * override is sent.
33
35
  * **No balance override is
34
36
  * applied** — `value` transfers are funded by the sender's real native
35
37
  * balance.
@@ -48,7 +50,8 @@ export declare function assertEndpointChain(params: {
48
50
  * no fallback backend.
49
51
  *
50
52
  * @param params - Shared simulation client, execution plan, already-pinned
51
- * state block, and the `validation` flag to send.
53
+ * state block, the `validation` flag to send, the optional simulated
54
+ * block gas limit, and whether to check the successor `parentHash`.
52
55
  * @returns Deep-frozen {@link SimulationExecution} — per-transaction call
53
56
  * results and the resolved {@link ExecutionBlock}.
54
57
  * @throws {ExternalServiceError} For transport failures, timeouts,
@@ -69,5 +72,7 @@ export declare function executePlan(params: {
69
72
  plan: ExecutionPlan;
70
73
  stateBlock: PinnedBlock;
71
74
  validation: boolean;
75
+ blockGasLimit?: bigint | undefined;
76
+ parentHashCheck: boolean;
72
77
  }): Promise<SimulationExecution>;
73
78
  export {};
@@ -92,7 +92,9 @@ async function assertEndpointChain(params) {
92
92
  * synthesizes native-ETH moves as transfer logs, and `validation: false`
93
93
  * (`true` on Monad, whose nodes reject `false` and charge no gas in the
94
94
  * simulated block). Validation-off means gas is not charged, which is how
95
- * gas is separated from economic effects.
95
+ * gas is separated from economic effects. When `blockGasLimit` is set,
96
+ * it is sent as the block's `blockOverrides.gasLimit`; otherwise no block
97
+ * override is sent.
96
98
  * **No balance override is
97
99
  * applied** — `value` transfers are funded by the sender's real native
98
100
  * balance.
@@ -111,7 +113,8 @@ async function assertEndpointChain(params) {
111
113
  * no fallback backend.
112
114
  *
113
115
  * @param params - Shared simulation client, execution plan, already-pinned
114
- * state block, and the `validation` flag to send.
116
+ * state block, the `validation` flag to send, the optional simulated
117
+ * block gas limit, and whether to check the successor `parentHash`.
115
118
  * @returns Deep-frozen {@link SimulationExecution} — per-transaction call
116
119
  * results and the resolved {@link ExecutionBlock}.
117
120
  * @throws {ExternalServiceError} For transport failures, timeouts,
@@ -128,13 +131,16 @@ async function assertEndpointChain(params) {
128
131
  * @internal
129
132
  */
130
133
  async function executePlan(params) {
131
- const { client, plan, stateBlock, validation } = params;
134
+ const { client, plan, stateBlock, validation, blockGasLimit, parentHashCheck, } = params;
132
135
  const response = await (0, exports.rpc)("eth_simulateV1", () => client.request({
133
136
  method: "eth_simulateV1",
134
137
  params: [
135
138
  {
136
139
  blockStateCalls: [
137
140
  {
141
+ ...(blockGasLimit !== undefined && {
142
+ blockOverrides: { gasLimit: (0, viem_1.numberToHex)(blockGasLimit) },
143
+ }),
138
144
  calls: plan.calls.map((call) => ({
139
145
  from: call.transaction.from,
140
146
  to: call.transaction.to,
@@ -160,6 +166,7 @@ async function executePlan(params) {
160
166
  stateBlockNumber: stateBlock.number,
161
167
  stateBlockHash: stateBlock.hash,
162
168
  stateBlockTimestamp: stateBlock.timestamp,
169
+ parentHashCheck,
163
170
  });
164
171
  // Reorg window: the pinned state block must still carry the same hash
165
172
  // after simulation, or the result may describe a different chain tip.
@@ -65,5 +65,6 @@ export declare function parseSimulationResponse(params: {
65
65
  readonly stateBlockNumber: bigint;
66
66
  readonly stateBlockHash: Hex;
67
67
  readonly stateBlockTimestamp: bigint;
68
+ readonly parentHashCheck: boolean;
68
69
  }): SimulationExecution;
69
70
  export {};
@@ -57,8 +57,10 @@ function parseSimulationResponse(params) {
57
57
  }
58
58
  // Anvil re-hashes the pinned block, so only the geth-style successor can be
59
59
  // pinned by hash: it must report a parentHash (geth always does) equal to
60
- // the pinned state block hash.
61
- if (blockNumber === params.stateBlockNumber + 1n &&
60
+ // the pinned state block hash. With `parentHashCheck` off (Stable by
61
+ // default) the block number, timestamp and reorg checks still apply.
62
+ if (params.parentHashCheck &&
63
+ blockNumber === params.stateBlockNumber + 1n &&
62
64
  block.parentHash !== params.stateBlockHash) {
63
65
  throw new errors_js_1.InvalidSimulationResponseError(`eth_simulateV1 reported block ${blockNumber} whose parent ${String(block.parentHash)} is not the pinned state block hash ${params.stateBlockHash}; the node did not simulate on top of the pinned block.`, { context: errorContext });
64
66
  }
@@ -4,6 +4,10 @@ import type { ChainSimulationConfig, SimulationConfig } from "../../types.js";
4
4
  *
5
5
  * Looks up the per-chain `ChainSimulationConfig`. Throws `UnsupportedChainError`
6
6
  * when the chain is absent from the map or has no `simulateV1Url` (defensive —
7
- * the type already requires it at construction).
7
+ * the type already requires it at construction). Throws
8
+ * `SimulationValidationError` when `blockOverrides.gasLimit` is set but is
9
+ * not a positive bigint, or `parentHashCheck` is set but is not a boolean, so a
10
+ * misconfiguration never surfaces as a bypassable
11
+ * `ExternalServiceError`.
8
12
  */
9
13
  export declare function resolveChain(config: SimulationConfig, chainId: number): ChainSimulationConfig;
@@ -7,11 +7,22 @@ const errors_js_1 = require("../../errors.js");
7
7
  *
8
8
  * Looks up the per-chain `ChainSimulationConfig`. Throws `UnsupportedChainError`
9
9
  * when the chain is absent from the map or has no `simulateV1Url` (defensive —
10
- * the type already requires it at construction).
10
+ * the type already requires it at construction). Throws
11
+ * `SimulationValidationError` when `blockOverrides.gasLimit` is set but is
12
+ * not a positive bigint, or `parentHashCheck` is set but is not a boolean, so a
13
+ * misconfiguration never surfaces as a bypassable
14
+ * `ExternalServiceError`.
11
15
  */
12
16
  function resolveChain(config, chainId) {
13
17
  const entry = config.chains.get(chainId);
14
18
  if (!entry?.simulateV1Url)
15
19
  throw new errors_js_1.UnsupportedChainError(chainId);
20
+ const gasLimit = entry.blockOverrides?.gasLimit;
21
+ if (gasLimit !== undefined &&
22
+ (typeof gasLimit !== "bigint" || gasLimit <= 0n))
23
+ throw new errors_js_1.SimulationValidationError(`Chain ${chainId} blockOverrides.gasLimit must be a positive bigint, got "${String(gasLimit)}" (${typeof gasLimit}). Pass a bigint greater than 0n or omit gasLimit.`, ["blockOverrides.gasLimit: must be a positive bigint"]);
24
+ const parentHashCheck = entry.parentHashCheck;
25
+ if (parentHashCheck !== undefined && typeof parentHashCheck !== "boolean")
26
+ throw new errors_js_1.SimulationValidationError(`Chain ${chainId} parentHashCheck must be a boolean, got "${String(parentHashCheck)}" (${typeof parentHashCheck}). Pass true or false, or omit parentHashCheck.`, ["parentHashCheck: must be a boolean"]);
16
27
  return entry;
17
28
  }
@@ -91,6 +91,8 @@ async function runSimulation(params) {
91
91
  plan,
92
92
  stateBlock: pinnedBlock,
93
93
  validation: monad,
94
+ blockGasLimit: chain.blockOverrides?.gasLimit,
95
+ parentHashCheck: chain.parentHashCheck ?? request.chainId !== blue_sdk_1.ChainId.StableMainnet,
94
96
  });
95
97
  const userCalls = execution.calls
96
98
  .filter((call) => call.planned.type === "transaction")
@@ -37,8 +37,9 @@ import type { SimulationConfig } from "../types.js";
37
37
  * separating gas from economic effects. Monad nodes reject `false`, so Monad
38
38
  * simulations send `true`; its simulated block charges no gas either way.
39
39
  *
40
- * @param config - Required per-chain `eth_simulateV1` URL, optional logger, and
41
- * the overall timeout budget.
40
+ * @param config - Required per-chain `eth_simulateV1` URL, optional per-chain
41
+ * `blockOverrides.gasLimit` and `parentHashCheck`, optional logger, and the
42
+ * overall timeout budget.
42
43
  * @param params - Per-call simulation input.
43
44
  * @param params.chainId - Chain id the bundle targets; must match the endpoint.
44
45
  * @param params.transactions - The bundle's transactions, in execution order.
@@ -52,7 +53,9 @@ import type { SimulationConfig } from "../types.js";
52
53
  * final), resolved exactly once.
53
54
  * @throws {SimulationValidationError} for invalid input (mixed senders, bad
54
55
  * addresses, empty transactions, malformed authorizations, final-mode
55
- * authorizations, malformed limits, unknown fields, a `"pending"` block tag, or share quotes for
56
+ * authorizations, malformed limits, unknown fields, a `"pending"` block tag, a
57
+ * `blockOverrides.gasLimit` that is not a positive bigint, a `parentHashCheck`
58
+ * that is not a boolean, or share quotes for
56
59
  * `blueSupplyCollateral` / `blueWithdrawCollateral`).
57
60
  * @throws {ConsumerLimitViolationError} when a declared `limits` bound is
58
61
  * violated by the observed effects.
@@ -66,7 +69,7 @@ import type { SimulationConfig } from "../types.js";
66
69
  * @throws {InvalidSimulationResponseError} when the node response cannot be
67
70
  * trusted (bad shape, call-count mismatch, block that is neither the pinned
68
71
  * state block nor its immediate successor, a successor with a mismatched
69
- * `parentHash`, a block timestamp earlier than the pinned block's, a
72
+ * `parentHash` (unless `parentHashCheck` is off; off by default on Stable, chain 988), a block timestamp earlier than the pinned block's, a
70
73
  * malformed per-call result, a quoted balance/position read whose non-empty
71
74
  * return data cannot be decoded, a state-block hash that changed or a pinned
72
75
  * block that vanished mid-flight, or an
@@ -39,8 +39,9 @@ const run_simulation_js_1 = require("./run-simulation.js");
39
39
  * separating gas from economic effects. Monad nodes reject `false`, so Monad
40
40
  * simulations send `true`; its simulated block charges no gas either way.
41
41
  *
42
- * @param config - Required per-chain `eth_simulateV1` URL, optional logger, and
43
- * the overall timeout budget.
42
+ * @param config - Required per-chain `eth_simulateV1` URL, optional per-chain
43
+ * `blockOverrides.gasLimit` and `parentHashCheck`, optional logger, and the
44
+ * overall timeout budget.
44
45
  * @param params - Per-call simulation input.
45
46
  * @param params.chainId - Chain id the bundle targets; must match the endpoint.
46
47
  * @param params.transactions - The bundle's transactions, in execution order.
@@ -54,7 +55,9 @@ const run_simulation_js_1 = require("./run-simulation.js");
54
55
  * final), resolved exactly once.
55
56
  * @throws {SimulationValidationError} for invalid input (mixed senders, bad
56
57
  * addresses, empty transactions, malformed authorizations, final-mode
57
- * authorizations, malformed limits, unknown fields, a `"pending"` block tag, or share quotes for
58
+ * authorizations, malformed limits, unknown fields, a `"pending"` block tag, a
59
+ * `blockOverrides.gasLimit` that is not a positive bigint, a `parentHashCheck`
60
+ * that is not a boolean, or share quotes for
58
61
  * `blueSupplyCollateral` / `blueWithdrawCollateral`).
59
62
  * @throws {ConsumerLimitViolationError} when a declared `limits` bound is
60
63
  * violated by the observed effects.
@@ -68,7 +71,7 @@ const run_simulation_js_1 = require("./run-simulation.js");
68
71
  * @throws {InvalidSimulationResponseError} when the node response cannot be
69
72
  * trusted (bad shape, call-count mismatch, block that is neither the pinned
70
73
  * state block nor its immediate successor, a successor with a mismatched
71
- * `parentHash`, a block timestamp earlier than the pinned block's, a
74
+ * `parentHash` (unless `parentHashCheck` is off; off by default on Stable, chain 988), a block timestamp earlier than the pinned block's, a
72
75
  * malformed per-call result, a quoted balance/position read whose non-empty
73
76
  * return data cannot be decoded, a state-block hash that changed or a pinned
74
77
  * block that vanished mid-flight, or an
@@ -3,6 +3,21 @@ import type { Address, Hex } from "viem";
3
3
  export interface ChainSimulationConfig {
4
4
  /** JSON-RPC URL supporting `eth_simulateV1`. Required for every configured chain. */
5
5
  readonly simulateV1Url: string;
6
+ /**
7
+ * Block overrides forwarded to `eth_simulateV1` as the simulated block's
8
+ * `blockOverrides`. Omitted fields leave the node's defaults untouched.
9
+ */
10
+ readonly blockOverrides?: {
11
+ /** Gas limit of the simulated block. Omitted: no gas limit override is sent. */
12
+ readonly gasLimit?: bigint;
13
+ };
14
+ /**
15
+ * Whether a simulated successor block's `parentHash` must equal the pinned
16
+ * state block hash. Block number, timestamp and reorg checks always run.
17
+ * Omitted: on everywhere except Stable (988), whose nodes report a
18
+ * `parentHash` that never matches the pinned block.
19
+ */
20
+ readonly parentHashCheck?: boolean;
6
21
  }
7
22
  /**
8
23
  * Top-level configuration for `simulate`.
@@ -29,7 +29,9 @@ export declare function assertEndpointChain(params: {
29
29
  * synthesizes native-ETH moves as transfer logs, and `validation: false`
30
30
  * (`true` on Monad, whose nodes reject `false` and charge no gas in the
31
31
  * simulated block). Validation-off means gas is not charged, which is how
32
- * gas is separated from economic effects.
32
+ * gas is separated from economic effects. When `blockGasLimit` is set,
33
+ * it is sent as the block's `blockOverrides.gasLimit`; otherwise no block
34
+ * override is sent.
33
35
  * **No balance override is
34
36
  * applied** — `value` transfers are funded by the sender's real native
35
37
  * balance.
@@ -48,7 +50,8 @@ export declare function assertEndpointChain(params: {
48
50
  * no fallback backend.
49
51
  *
50
52
  * @param params - Shared simulation client, execution plan, already-pinned
51
- * state block, and the `validation` flag to send.
53
+ * state block, the `validation` flag to send, the optional simulated
54
+ * block gas limit, and whether to check the successor `parentHash`.
52
55
  * @returns Deep-frozen {@link SimulationExecution} — per-transaction call
53
56
  * results and the resolved {@link ExecutionBlock}.
54
57
  * @throws {ExternalServiceError} For transport failures, timeouts,
@@ -69,5 +72,7 @@ export declare function executePlan(params: {
69
72
  plan: ExecutionPlan;
70
73
  stateBlock: PinnedBlock;
71
74
  validation: boolean;
75
+ blockGasLimit?: bigint | undefined;
76
+ parentHashCheck: boolean;
72
77
  }): Promise<SimulationExecution>;
73
78
  export {};
@@ -86,7 +86,9 @@ export async function assertEndpointChain(params) {
86
86
  * synthesizes native-ETH moves as transfer logs, and `validation: false`
87
87
  * (`true` on Monad, whose nodes reject `false` and charge no gas in the
88
88
  * simulated block). Validation-off means gas is not charged, which is how
89
- * gas is separated from economic effects.
89
+ * gas is separated from economic effects. When `blockGasLimit` is set,
90
+ * it is sent as the block's `blockOverrides.gasLimit`; otherwise no block
91
+ * override is sent.
90
92
  * **No balance override is
91
93
  * applied** — `value` transfers are funded by the sender's real native
92
94
  * balance.
@@ -105,7 +107,8 @@ export async function assertEndpointChain(params) {
105
107
  * no fallback backend.
106
108
  *
107
109
  * @param params - Shared simulation client, execution plan, already-pinned
108
- * state block, and the `validation` flag to send.
110
+ * state block, the `validation` flag to send, the optional simulated
111
+ * block gas limit, and whether to check the successor `parentHash`.
109
112
  * @returns Deep-frozen {@link SimulationExecution} — per-transaction call
110
113
  * results and the resolved {@link ExecutionBlock}.
111
114
  * @throws {ExternalServiceError} For transport failures, timeouts,
@@ -122,13 +125,16 @@ export async function assertEndpointChain(params) {
122
125
  * @internal
123
126
  */
124
127
  export async function executePlan(params) {
125
- const { client, plan, stateBlock, validation } = params;
128
+ const { client, plan, stateBlock, validation, blockGasLimit, parentHashCheck, } = params;
126
129
  const response = await rpc("eth_simulateV1", () => client.request({
127
130
  method: "eth_simulateV1",
128
131
  params: [
129
132
  {
130
133
  blockStateCalls: [
131
134
  {
135
+ ...(blockGasLimit !== undefined && {
136
+ blockOverrides: { gasLimit: numberToHex(blockGasLimit) },
137
+ }),
132
138
  calls: plan.calls.map((call) => ({
133
139
  from: call.transaction.from,
134
140
  to: call.transaction.to,
@@ -154,6 +160,7 @@ export async function executePlan(params) {
154
160
  stateBlockNumber: stateBlock.number,
155
161
  stateBlockHash: stateBlock.hash,
156
162
  stateBlockTimestamp: stateBlock.timestamp,
163
+ parentHashCheck,
157
164
  });
158
165
  // Reorg window: the pinned state block must still carry the same hash
159
166
  // after simulation, or the result may describe a different chain tip.
@@ -65,5 +65,6 @@ export declare function parseSimulationResponse(params: {
65
65
  readonly stateBlockNumber: bigint;
66
66
  readonly stateBlockHash: Hex;
67
67
  readonly stateBlockTimestamp: bigint;
68
+ readonly parentHashCheck: boolean;
68
69
  }): SimulationExecution;
69
70
  export {};
@@ -54,8 +54,10 @@ export function parseSimulationResponse(params) {
54
54
  }
55
55
  // Anvil re-hashes the pinned block, so only the geth-style successor can be
56
56
  // pinned by hash: it must report a parentHash (geth always does) equal to
57
- // the pinned state block hash.
58
- if (blockNumber === params.stateBlockNumber + 1n &&
57
+ // the pinned state block hash. With `parentHashCheck` off (Stable by
58
+ // default) the block number, timestamp and reorg checks still apply.
59
+ if (params.parentHashCheck &&
60
+ blockNumber === params.stateBlockNumber + 1n &&
59
61
  block.parentHash !== params.stateBlockHash) {
60
62
  throw new InvalidSimulationResponseError(`eth_simulateV1 reported block ${blockNumber} whose parent ${String(block.parentHash)} is not the pinned state block hash ${params.stateBlockHash}; the node did not simulate on top of the pinned block.`, { context: errorContext });
61
63
  }
@@ -4,6 +4,10 @@ import type { ChainSimulationConfig, SimulationConfig } from "../../types.js";
4
4
  *
5
5
  * Looks up the per-chain `ChainSimulationConfig`. Throws `UnsupportedChainError`
6
6
  * when the chain is absent from the map or has no `simulateV1Url` (defensive —
7
- * the type already requires it at construction).
7
+ * the type already requires it at construction). Throws
8
+ * `SimulationValidationError` when `blockOverrides.gasLimit` is set but is
9
+ * not a positive bigint, or `parentHashCheck` is set but is not a boolean, so a
10
+ * misconfiguration never surfaces as a bypassable
11
+ * `ExternalServiceError`.
8
12
  */
9
13
  export declare function resolveChain(config: SimulationConfig, chainId: number): ChainSimulationConfig;
@@ -1,14 +1,25 @@
1
- import { UnsupportedChainError } from "../../errors.js";
1
+ import { SimulationValidationError, UnsupportedChainError, } from "../../errors.js";
2
2
  /**
3
3
  * Stage 2 of the simulate() pipeline.
4
4
  *
5
5
  * Looks up the per-chain `ChainSimulationConfig`. Throws `UnsupportedChainError`
6
6
  * when the chain is absent from the map or has no `simulateV1Url` (defensive —
7
- * the type already requires it at construction).
7
+ * the type already requires it at construction). Throws
8
+ * `SimulationValidationError` when `blockOverrides.gasLimit` is set but is
9
+ * not a positive bigint, or `parentHashCheck` is set but is not a boolean, so a
10
+ * misconfiguration never surfaces as a bypassable
11
+ * `ExternalServiceError`.
8
12
  */
9
13
  export function resolveChain(config, chainId) {
10
14
  const entry = config.chains.get(chainId);
11
15
  if (!entry?.simulateV1Url)
12
16
  throw new UnsupportedChainError(chainId);
17
+ const gasLimit = entry.blockOverrides?.gasLimit;
18
+ if (gasLimit !== undefined &&
19
+ (typeof gasLimit !== "bigint" || gasLimit <= 0n))
20
+ throw new SimulationValidationError(`Chain ${chainId} blockOverrides.gasLimit must be a positive bigint, got "${String(gasLimit)}" (${typeof gasLimit}). Pass a bigint greater than 0n or omit gasLimit.`, ["blockOverrides.gasLimit: must be a positive bigint"]);
21
+ const parentHashCheck = entry.parentHashCheck;
22
+ if (parentHashCheck !== undefined && typeof parentHashCheck !== "boolean")
23
+ throw new SimulationValidationError(`Chain ${chainId} parentHashCheck must be a boolean, got "${String(parentHashCheck)}" (${typeof parentHashCheck}). Pass true or false, or omit parentHashCheck.`, ["parentHashCheck: must be a boolean"]);
13
24
  return entry;
14
25
  }
@@ -88,6 +88,8 @@ export async function runSimulation(params) {
88
88
  plan,
89
89
  stateBlock: pinnedBlock,
90
90
  validation: monad,
91
+ blockGasLimit: chain.blockOverrides?.gasLimit,
92
+ parentHashCheck: chain.parentHashCheck ?? request.chainId !== ChainId.StableMainnet,
91
93
  });
92
94
  const userCalls = execution.calls
93
95
  .filter((call) => call.planned.type === "transaction")
@@ -37,8 +37,9 @@ import type { SimulationConfig } from "../types.js";
37
37
  * separating gas from economic effects. Monad nodes reject `false`, so Monad
38
38
  * simulations send `true`; its simulated block charges no gas either way.
39
39
  *
40
- * @param config - Required per-chain `eth_simulateV1` URL, optional logger, and
41
- * the overall timeout budget.
40
+ * @param config - Required per-chain `eth_simulateV1` URL, optional per-chain
41
+ * `blockOverrides.gasLimit` and `parentHashCheck`, optional logger, and the
42
+ * overall timeout budget.
42
43
  * @param params - Per-call simulation input.
43
44
  * @param params.chainId - Chain id the bundle targets; must match the endpoint.
44
45
  * @param params.transactions - The bundle's transactions, in execution order.
@@ -52,7 +53,9 @@ import type { SimulationConfig } from "../types.js";
52
53
  * final), resolved exactly once.
53
54
  * @throws {SimulationValidationError} for invalid input (mixed senders, bad
54
55
  * addresses, empty transactions, malformed authorizations, final-mode
55
- * authorizations, malformed limits, unknown fields, a `"pending"` block tag, or share quotes for
56
+ * authorizations, malformed limits, unknown fields, a `"pending"` block tag, a
57
+ * `blockOverrides.gasLimit` that is not a positive bigint, a `parentHashCheck`
58
+ * that is not a boolean, or share quotes for
56
59
  * `blueSupplyCollateral` / `blueWithdrawCollateral`).
57
60
  * @throws {ConsumerLimitViolationError} when a declared `limits` bound is
58
61
  * violated by the observed effects.
@@ -66,7 +69,7 @@ import type { SimulationConfig } from "../types.js";
66
69
  * @throws {InvalidSimulationResponseError} when the node response cannot be
67
70
  * trusted (bad shape, call-count mismatch, block that is neither the pinned
68
71
  * state block nor its immediate successor, a successor with a mismatched
69
- * `parentHash`, a block timestamp earlier than the pinned block's, a
72
+ * `parentHash` (unless `parentHashCheck` is off; off by default on Stable, chain 988), a block timestamp earlier than the pinned block's, a
70
73
  * malformed per-call result, a quoted balance/position read whose non-empty
71
74
  * return data cannot be decoded, a state-block hash that changed or a pinned
72
75
  * block that vanished mid-flight, or an
@@ -36,8 +36,9 @@ import { runSimulation } from "./run-simulation.js";
36
36
  * separating gas from economic effects. Monad nodes reject `false`, so Monad
37
37
  * simulations send `true`; its simulated block charges no gas either way.
38
38
  *
39
- * @param config - Required per-chain `eth_simulateV1` URL, optional logger, and
40
- * the overall timeout budget.
39
+ * @param config - Required per-chain `eth_simulateV1` URL, optional per-chain
40
+ * `blockOverrides.gasLimit` and `parentHashCheck`, optional logger, and the
41
+ * overall timeout budget.
41
42
  * @param params - Per-call simulation input.
42
43
  * @param params.chainId - Chain id the bundle targets; must match the endpoint.
43
44
  * @param params.transactions - The bundle's transactions, in execution order.
@@ -51,7 +52,9 @@ import { runSimulation } from "./run-simulation.js";
51
52
  * final), resolved exactly once.
52
53
  * @throws {SimulationValidationError} for invalid input (mixed senders, bad
53
54
  * addresses, empty transactions, malformed authorizations, final-mode
54
- * authorizations, malformed limits, unknown fields, a `"pending"` block tag, or share quotes for
55
+ * authorizations, malformed limits, unknown fields, a `"pending"` block tag, a
56
+ * `blockOverrides.gasLimit` that is not a positive bigint, a `parentHashCheck`
57
+ * that is not a boolean, or share quotes for
55
58
  * `blueSupplyCollateral` / `blueWithdrawCollateral`).
56
59
  * @throws {ConsumerLimitViolationError} when a declared `limits` bound is
57
60
  * violated by the observed effects.
@@ -65,7 +68,7 @@ import { runSimulation } from "./run-simulation.js";
65
68
  * @throws {InvalidSimulationResponseError} when the node response cannot be
66
69
  * trusted (bad shape, call-count mismatch, block that is neither the pinned
67
70
  * state block nor its immediate successor, a successor with a mismatched
68
- * `parentHash`, a block timestamp earlier than the pinned block's, a
71
+ * `parentHash` (unless `parentHashCheck` is off; off by default on Stable, chain 988), a block timestamp earlier than the pinned block's, a
69
72
  * malformed per-call result, a quoted balance/position read whose non-empty
70
73
  * return data cannot be decoded, a state-block hash that changed or a pinned
71
74
  * block that vanished mid-flight, or an
@@ -3,6 +3,21 @@ import type { Address, Hex } from "viem";
3
3
  export interface ChainSimulationConfig {
4
4
  /** JSON-RPC URL supporting `eth_simulateV1`. Required for every configured chain. */
5
5
  readonly simulateV1Url: string;
6
+ /**
7
+ * Block overrides forwarded to `eth_simulateV1` as the simulated block's
8
+ * `blockOverrides`. Omitted fields leave the node's defaults untouched.
9
+ */
10
+ readonly blockOverrides?: {
11
+ /** Gas limit of the simulated block. Omitted: no gas limit override is sent. */
12
+ readonly gasLimit?: bigint;
13
+ };
14
+ /**
15
+ * Whether a simulated successor block's `parentHash` must equal the pinned
16
+ * state block hash. Block number, timestamp and reorg checks always run.
17
+ * Omitted: on everywhere except Stable (988), whose nodes report a
18
+ * `parentHash` that never matches the pinned block.
19
+ */
20
+ readonly parentHashCheck?: boolean;
6
21
  }
7
22
  /**
8
23
  * Top-level configuration for `simulate`.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@morpho-org/evm-simulation",
3
3
  "description": "EVM simulation engine for Morpho transactions.",
4
- "version": "5.0.1",
4
+ "version": "5.1.0",
5
5
  "author": "Morpho Association <contact@morpho.org>",
6
6
  "repository": {
7
7
  "type": "git",