@morpho-org/evm-simulation 5.0.1 → 6.0.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.
Files changed (50) hide show
  1. package/README.md +6 -4
  2. package/lib/cjs/errors.d.ts +17 -2
  3. package/lib/cjs/errors.js +19 -3
  4. package/lib/cjs/index.d.ts +2 -2
  5. package/lib/cjs/index.js +2 -1
  6. package/lib/cjs/params.d.ts +20 -1
  7. package/lib/cjs/requirements/requirements.js +2 -1
  8. package/lib/cjs/simulate/backends/eth-simulate-v1.d.ts +26 -34
  9. package/lib/cjs/simulate/backends/eth-simulate-v1.js +35 -74
  10. package/lib/cjs/simulate/backends/index.d.ts +1 -1
  11. package/lib/cjs/simulate/backends/index.js +1 -2
  12. package/lib/cjs/simulate/backends/parse-response.d.ts +1 -0
  13. package/lib/cjs/simulate/backends/parse-response.js +4 -2
  14. package/lib/cjs/simulate/backends/resolve-pinned-block.d.ts +6 -12
  15. package/lib/cjs/simulate/backends/resolve-pinned-block.js +3 -4
  16. package/lib/cjs/simulate/pipeline/bundles-retention.d.ts +3 -3
  17. package/lib/cjs/simulate/pipeline/bundles-retention.js +6 -6
  18. package/lib/cjs/simulate/pipeline/resolve-chain.d.ts +5 -1
  19. package/lib/cjs/simulate/pipeline/resolve-chain.js +12 -1
  20. package/lib/cjs/simulate/request/parse-request.d.ts +2 -1
  21. package/lib/cjs/simulate/request/parse-request.js +21 -0
  22. package/lib/cjs/simulate/run-simulation.js +9 -12
  23. package/lib/cjs/simulate/simulate.d.ts +30 -19
  24. package/lib/cjs/simulate/simulate.js +30 -19
  25. package/lib/cjs/types.d.ts +15 -0
  26. package/lib/esm/errors.d.ts +17 -2
  27. package/lib/esm/errors.js +17 -2
  28. package/lib/esm/index.d.ts +2 -2
  29. package/lib/esm/index.js +1 -1
  30. package/lib/esm/params.d.ts +20 -1
  31. package/lib/esm/requirements/requirements.js +2 -1
  32. package/lib/esm/simulate/backends/eth-simulate-v1.d.ts +26 -34
  33. package/lib/esm/simulate/backends/eth-simulate-v1.js +37 -75
  34. package/lib/esm/simulate/backends/index.d.ts +1 -1
  35. package/lib/esm/simulate/backends/index.js +1 -1
  36. package/lib/esm/simulate/backends/parse-response.d.ts +1 -0
  37. package/lib/esm/simulate/backends/parse-response.js +4 -2
  38. package/lib/esm/simulate/backends/resolve-pinned-block.d.ts +6 -12
  39. package/lib/esm/simulate/backends/resolve-pinned-block.js +3 -4
  40. package/lib/esm/simulate/pipeline/bundles-retention.d.ts +3 -3
  41. package/lib/esm/simulate/pipeline/bundles-retention.js +6 -6
  42. package/lib/esm/simulate/pipeline/resolve-chain.d.ts +5 -1
  43. package/lib/esm/simulate/pipeline/resolve-chain.js +13 -2
  44. package/lib/esm/simulate/request/parse-request.d.ts +2 -1
  45. package/lib/esm/simulate/request/parse-request.js +21 -0
  46. package/lib/esm/simulate/run-simulation.js +10 -13
  47. package/lib/esm/simulate/simulate.d.ts +30 -19
  48. package/lib/esm/simulate/simulate.js +30 -19
  49. package/lib/esm/types.d.ts +15 -0
  50. package/package.json +4 -4
@@ -1,63 +1,53 @@
1
- import { type Client, type PublicClient } from "viem";
2
- import type { SimulationMode } from "../../params.js";
1
+ import { type Client } from "viem";
2
+ import type { StateBlock } from "../../params.js";
3
3
  import type { ExecutionPlan } from "../plan/plan-execution.js";
4
4
  import type { SimulationExecution } from "./parse-response.js";
5
- import type { PinnedBlock } from "./resolve-pinned-block.js";
6
5
  /** The JSON-RPC methods this boundary calls. */
7
- type RpcLabel = "eth_getBlock" | "eth_chainId" | "eth_simulateV1";
6
+ type RpcLabel = "eth_getBlock" | "eth_simulateV1";
8
7
  /** Run one RPC call; anything thrown becomes a typed boundary error. @internal */
9
8
  export declare const rpc: <T>(label: RpcLabel, call: () => Promise<T>) => Promise<T>;
10
- /** Validate the endpoint identity through the same URL-safe RPC boundary. @internal */
11
- export declare function assertEndpointChain(params: {
12
- readonly client: Pick<PublicClient, "getChainId">;
13
- readonly chainId: number;
14
- readonly mode: SimulationMode;
15
- readonly blockNumber?: bigint;
16
- }): Promise<void>;
17
9
  /**
18
10
  * Execute an {@link ExecutionPlan} through a single `eth_simulateV1` call and
19
- * collect the pinned execution.
11
+ * collect the pinned execution. This is the boundary's only RPC request.
20
12
  *
21
- * The caller checks the endpoint chain identity and resolves the pinned state
22
- * block before this boundary. This function uses that same block for the
23
- * simulation and reorg check under the shared abort/timeout budget.
24
- *
25
- * The boundary performs these steps:
13
+ * The caller resolves (or supplies) the state block before this boundary.
26
14
  *
27
15
  * 1. **`eth_simulateV1`** — one `blockStateCalls` entry carrying the planned
28
- * calls with their per-call `from`, `traceTransfers: true` so the node
29
- * synthesizes native-ETH moves as transfer logs, and `validation: false`
16
+ * calls with their per-call `from` and `chainId`, `traceTransfers: true`
17
+ * so the node synthesizes native-ETH moves as transfer logs, and `validation: false`
30
18
  * (`true` on Monad, whose nodes reject `false` and charge no gas in the
31
19
  * simulated block). Validation-off means gas is not charged, which is how
32
- * gas is separated from economic effects.
20
+ * gas is separated from economic effects. When `blockGasLimit` is set,
21
+ * it is sent as the block's `blockOverrides.gasLimit`; otherwise no block
22
+ * override is sent.
33
23
  * **No balance override is
34
24
  * applied** — `value` transfers are funded by the sender's real native
35
25
  * balance.
36
- * 2. **Response validation** — the response is parsed before the reorg
37
- * re-fetch so revert/mismatch evidence already in hand surfaces instead
38
- * of being downgraded to a bypassable {@link ExternalServiceError} by a
39
- * failing re-fetch. The simulated block must be exactly
26
+ * 2. **Response validation** — the simulated block must be exactly
40
27
  * `stateBlockNumber` or `stateBlockNumber + 1`: geth-style nodes report
41
28
  * the former's successor while Anvil reports the pinned block itself.
42
29
  * The result records whatever the node returns; consumers must read
43
30
  * {@link ExecutionBlock.blockNumber} and never assume +1.
44
- * 3. **Reorg check** — the pinned state block is re-fetched last to detect a reorg
45
- * that swapped its hash mid-flight (`InvalidSimulationResponseError`).
46
31
  *
47
32
  * The endpoint must support `eth_simulateV1` with per-call `from`; there is
48
33
  * no fallback backend.
49
34
  *
50
- * @param params - Shared simulation client, execution plan, already-pinned
51
- * state block, and the `validation` flag to send.
35
+ * @param params - Shared simulation client, execution plan (its request
36
+ * `chainId` is sent on every call), state block, the `validation` flag to
37
+ * send, the optional simulated block gas limit, and whether to check the
38
+ * successor `parentHash`.
52
39
  * @returns Deep-frozen {@link SimulationExecution} — per-transaction call
53
40
  * results and the resolved {@link ExecutionBlock}.
54
41
  * @throws {ExternalServiceError} For transport failures, timeouts,
55
42
  * or malformed JSON-RPC envelopes.
56
43
  * @throws {InvalidSimulationResponseError} For a response that cannot be
57
- * trusted (bad shape, call-count mismatch, block
58
- * other than the pinned state block or its successor, a block timestamp
59
- * earlier than the pinned block's, a per-call result that fails normalization,
60
- * or a state-block hash that changed or is no longer served mid-flight).
44
+ * trusted (bad shape, call-count mismatch, block other than the state block
45
+ * or its successor, a block timestamp earlier than the state block's, or a
46
+ * per-call result that fails normalization).
47
+ * @throws {InvalidChainIdError} When the node rejects the request `chainId`
48
+ * because it serves another chain (a subclass of
49
+ * `InvalidSimulationResponseError` sharing its `name`/`code`;
50
+ * `instanceof InvalidChainIdError` tells them apart).
61
51
  * @throws {MissingVerificationEvidenceError} When a planned state read fails.
62
52
  * @throws {SimulationRevertedError} When a preparation or user transaction
63
53
  * reverts or the node reports a bundle-level revert (code 3 / insufficient
@@ -65,9 +55,11 @@ export declare function assertEndpointChain(params: {
65
55
  * @internal
66
56
  */
67
57
  export declare function executePlan(params: {
68
- client: Client & Pick<PublicClient, "getBlock">;
58
+ client: Client;
69
59
  plan: ExecutionPlan;
70
- stateBlock: PinnedBlock;
60
+ stateBlock: StateBlock;
71
61
  validation: boolean;
62
+ blockGasLimit?: bigint | undefined;
63
+ parentHashCheck: boolean;
72
64
  }): Promise<SimulationExecution>;
73
65
  export {};
@@ -1,5 +1,5 @@
1
- import { BaseError, BlockNotFoundError, ExecutionRevertedError, InsufficientFundsError, numberToHex, } from "viem";
2
- import { ExternalServiceError, InvalidSimulationResponseError, SimulationPackageError, SimulationRevertedError, } from "../../errors.js";
1
+ import { BaseError, ExecutionRevertedError, InsufficientFundsError, numberToHex, } from "viem";
2
+ import { ExternalServiceError, InvalidChainIdError, SimulationPackageError, SimulationRevertedError, } from "../../errors.js";
3
3
  import { parseSimulationResponse } from "./parse-response.js";
4
4
  /**
5
5
  * Classify a node-level revert: viem's `ExecutionRevertedError`, or a raw
@@ -39,6 +39,15 @@ const toBoundaryError = (label, error) => {
39
39
  reverted.cause = error;
40
40
  return reverted;
41
41
  }
42
+ // A wrong-chain endpoint is misconfiguration, not an outage: callers must
43
+ // not bypass it like an `ExternalServiceError`. Each node words the per-call
44
+ // `chainId` rejection differently: geth "chainId does not match node's",
45
+ // Anvil "invalid chain id for signer", Monad "Invalid chain ID: expected …".
46
+ if (label === "eth_simulateV1" &&
47
+ error instanceof Error &&
48
+ /chainId does not match node's|invalid chain id/i.test(error.message)) {
49
+ return new InvalidChainIdError(`eth_simulateV1 endpoint rejected the request chainId: ${safeMessage(error)}. Point SimulationConfig.chains at an RPC URL for this chain.`, { cause: error });
50
+ }
42
51
  return new ExternalServiceError(`${label} error: ${safeMessage(error)}`, {
43
52
  cause: error,
44
53
  });
@@ -52,69 +61,48 @@ export const rpc = async (label, call) => {
52
61
  throw toBoundaryError(label, error);
53
62
  }
54
63
  };
55
- /** Validate the endpoint identity through the same URL-safe RPC boundary. @internal */
56
- export async function assertEndpointChain(params) {
57
- const { client, chainId, mode, blockNumber } = params;
58
- const rpcChainId = await rpc("eth_chainId", () => client.getChainId());
59
- if (rpcChainId === chainId)
60
- return;
61
- throw new InvalidSimulationResponseError(`The RPC configured for chain ${chainId} reports chain ${rpcChainId}. Fix SimulationConfig.chains.`, {
62
- ...(blockNumber !== undefined
63
- ? {
64
- context: {
65
- stage: "transport",
66
- chainId,
67
- mode,
68
- blockNumber,
69
- },
70
- }
71
- : {}),
72
- });
73
- }
74
64
  /**
75
65
  * Execute an {@link ExecutionPlan} through a single `eth_simulateV1` call and
76
- * collect the pinned execution.
77
- *
78
- * The caller checks the endpoint chain identity and resolves the pinned state
79
- * block before this boundary. This function uses that same block for the
80
- * simulation and reorg check under the shared abort/timeout budget.
66
+ * collect the pinned execution. This is the boundary's only RPC request.
81
67
  *
82
- * The boundary performs these steps:
68
+ * The caller resolves (or supplies) the state block before this boundary.
83
69
  *
84
70
  * 1. **`eth_simulateV1`** — one `blockStateCalls` entry carrying the planned
85
- * calls with their per-call `from`, `traceTransfers: true` so the node
86
- * synthesizes native-ETH moves as transfer logs, and `validation: false`
71
+ * calls with their per-call `from` and `chainId`, `traceTransfers: true`
72
+ * so the node synthesizes native-ETH moves as transfer logs, and `validation: false`
87
73
  * (`true` on Monad, whose nodes reject `false` and charge no gas in the
88
74
  * simulated block). Validation-off means gas is not charged, which is how
89
- * gas is separated from economic effects.
75
+ * gas is separated from economic effects. When `blockGasLimit` is set,
76
+ * it is sent as the block's `blockOverrides.gasLimit`; otherwise no block
77
+ * override is sent.
90
78
  * **No balance override is
91
79
  * applied** — `value` transfers are funded by the sender's real native
92
80
  * balance.
93
- * 2. **Response validation** — the response is parsed before the reorg
94
- * re-fetch so revert/mismatch evidence already in hand surfaces instead
95
- * of being downgraded to a bypassable {@link ExternalServiceError} by a
96
- * failing re-fetch. The simulated block must be exactly
81
+ * 2. **Response validation** — the simulated block must be exactly
97
82
  * `stateBlockNumber` or `stateBlockNumber + 1`: geth-style nodes report
98
83
  * the former's successor while Anvil reports the pinned block itself.
99
84
  * The result records whatever the node returns; consumers must read
100
85
  * {@link ExecutionBlock.blockNumber} and never assume +1.
101
- * 3. **Reorg check** — the pinned state block is re-fetched last to detect a reorg
102
- * that swapped its hash mid-flight (`InvalidSimulationResponseError`).
103
86
  *
104
87
  * The endpoint must support `eth_simulateV1` with per-call `from`; there is
105
88
  * no fallback backend.
106
89
  *
107
- * @param params - Shared simulation client, execution plan, already-pinned
108
- * state block, and the `validation` flag to send.
90
+ * @param params - Shared simulation client, execution plan (its request
91
+ * `chainId` is sent on every call), state block, the `validation` flag to
92
+ * send, the optional simulated block gas limit, and whether to check the
93
+ * successor `parentHash`.
109
94
  * @returns Deep-frozen {@link SimulationExecution} — per-transaction call
110
95
  * results and the resolved {@link ExecutionBlock}.
111
96
  * @throws {ExternalServiceError} For transport failures, timeouts,
112
97
  * or malformed JSON-RPC envelopes.
113
98
  * @throws {InvalidSimulationResponseError} For a response that cannot be
114
- * trusted (bad shape, call-count mismatch, block
115
- * other than the pinned state block or its successor, a block timestamp
116
- * earlier than the pinned block's, a per-call result that fails normalization,
117
- * or a state-block hash that changed or is no longer served mid-flight).
99
+ * trusted (bad shape, call-count mismatch, block other than the state block
100
+ * or its successor, a block timestamp earlier than the state block's, or a
101
+ * per-call result that fails normalization).
102
+ * @throws {InvalidChainIdError} When the node rejects the request `chainId`
103
+ * because it serves another chain (a subclass of
104
+ * `InvalidSimulationResponseError` sharing its `name`/`code`;
105
+ * `instanceof InvalidChainIdError` tells them apart).
118
106
  * @throws {MissingVerificationEvidenceError} When a planned state read fails.
119
107
  * @throws {SimulationRevertedError} When a preparation or user transaction
120
108
  * reverts or the node reports a bundle-level revert (code 3 / insufficient
@@ -122,14 +110,18 @@ export async function assertEndpointChain(params) {
122
110
  * @internal
123
111
  */
124
112
  export async function executePlan(params) {
125
- const { client, plan, stateBlock, validation } = params;
113
+ const { client, plan, stateBlock, validation, blockGasLimit, parentHashCheck, } = params;
126
114
  const response = await rpc("eth_simulateV1", () => client.request({
127
115
  method: "eth_simulateV1",
128
116
  params: [
129
117
  {
130
118
  blockStateCalls: [
131
119
  {
120
+ ...(blockGasLimit !== undefined && {
121
+ blockOverrides: { gasLimit: numberToHex(blockGasLimit) },
122
+ }),
132
123
  calls: plan.calls.map((call) => ({
124
+ chainId: numberToHex(plan.request.chainId),
133
125
  from: call.transaction.from,
134
126
  to: call.transaction.to,
135
127
  data: call.transaction.data,
@@ -143,42 +135,12 @@ export async function executePlan(params) {
143
135
  numberToHex(stateBlock.number),
144
136
  ],
145
137
  }));
146
- // Response parsing is validation, not transport — it runs before the
147
- // reorg re-fetch so evidence already in hand reaches the caller as
148
- // InvalidSimulationResponseError / SimulationRevertedError instead of
149
- // being downgraded to a bypassable ExternalServiceError by a failing
150
- // re-fetch.
151
- const execution = parseSimulationResponse({
138
+ return parseSimulationResponse({
152
139
  plan,
153
140
  blocks: response,
154
141
  stateBlockNumber: stateBlock.number,
155
142
  stateBlockHash: stateBlock.hash,
156
143
  stateBlockTimestamp: stateBlock.timestamp,
144
+ parentHashCheck,
157
145
  });
158
- // Reorg window: the pinned state block must still carry the same hash
159
- // after simulation, or the result may describe a different chain tip.
160
- const stateBlockAfter = await rpc("eth_getBlock", () => client.getBlock({ blockNumber: stateBlock.number })).catch((error) => {
161
- if (error instanceof ExternalServiceError &&
162
- error.cause instanceof BlockNotFoundError)
163
- return error;
164
- throw error;
165
- });
166
- if (stateBlockAfter instanceof ExternalServiceError ||
167
- stateBlockAfter.hash !== stateBlock.hash) {
168
- const after = stateBlockAfter instanceof ExternalServiceError
169
- ? "unavailable"
170
- : stateBlockAfter.hash;
171
- throw new InvalidSimulationResponseError(`State block ${stateBlock.number} hash changed during simulation (reorg): ${stateBlock.hash} became ${after}. Re-submit the simulation.`, {
172
- context: {
173
- stage: "transport",
174
- chainId: plan.request.chainId,
175
- mode: plan.request.mode,
176
- blockNumber: stateBlock.number,
177
- },
178
- ...(stateBlockAfter instanceof ExternalServiceError && {
179
- cause: stateBlockAfter.cause,
180
- }),
181
- });
182
- }
183
- return execution;
184
146
  }
@@ -1 +1 @@
1
- export { assertEndpointChain, executePlan } from "./eth-simulate-v1.js";
1
+ export { executePlan } from "./eth-simulate-v1.js";
@@ -1 +1 @@
1
- export { assertEndpointChain, executePlan } from "./eth-simulate-v1.js";
1
+ export { executePlan } from "./eth-simulate-v1.js";
@@ -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 and timestamp 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
  }
@@ -1,16 +1,10 @@
1
- import type { BlockTag, Client, Hex } from "viem";
2
- /** One concrete canonical block: the state anchor for every pinned read. @internal */
3
- export interface PinnedBlock {
4
- readonly number: bigint;
5
- readonly hash: Hex;
6
- readonly timestamp: bigint;
7
- }
1
+ import type { BlockTag, Client } from "viem";
2
+ import type { StateBlock } from "../../params.js";
8
3
  /**
9
4
  * Resolve the requested state block exactly once so `latest` cannot drift.
10
5
  *
11
- * Every downstream read and the `eth_simulateV1` call pin this number; the
12
- * execution boundary re-fetches the hash afterwards to detect a reorg that
13
- * swapped the block mid-flight.
6
+ * Every downstream read and the `eth_simulateV1` call pin this number. Skipped
7
+ * when the caller supplies `SimulateParams.block`.
14
8
  *
15
9
  * @param params - Resolution parameters.
16
10
  * @param params.client - Shared simulation client (created by
@@ -19,7 +13,7 @@ export interface PinnedBlock {
19
13
  * `latest`.
20
14
  * @param params.signal - Optional pipeline abort signal; checked around the
21
15
  * request so an aborted pipeline never produces a pin.
22
- * @returns The resolved {@link PinnedBlock}.
16
+ * @returns The resolved {@link StateBlock}.
23
17
  * @throws {ExternalServiceError} For transport failures, timeouts, or an
24
18
  * aborted signal.
25
19
  * @throws {InvalidSimulationResponseError} When the node returns a block
@@ -30,4 +24,4 @@ export declare function resolvePinnedBlock(params: {
30
24
  readonly client: Client;
31
25
  readonly blockNumber?: bigint | BlockTag;
32
26
  readonly signal?: AbortSignal;
33
- }): Promise<PinnedBlock>;
27
+ }): Promise<StateBlock>;
@@ -4,9 +4,8 @@ import { rpc } from "./eth-simulate-v1.js";
4
4
  /**
5
5
  * Resolve the requested state block exactly once so `latest` cannot drift.
6
6
  *
7
- * Every downstream read and the `eth_simulateV1` call pin this number; the
8
- * execution boundary re-fetches the hash afterwards to detect a reorg that
9
- * swapped the block mid-flight.
7
+ * Every downstream read and the `eth_simulateV1` call pin this number. Skipped
8
+ * when the caller supplies `SimulateParams.block`.
10
9
  *
11
10
  * @param params - Resolution parameters.
12
11
  * @param params.client - Shared simulation client (created by
@@ -15,7 +14,7 @@ import { rpc } from "./eth-simulate-v1.js";
15
14
  * `latest`.
16
15
  * @param params.signal - Optional pipeline abort signal; checked around the
17
16
  * request so an aborted pipeline never produces a pin.
18
- * @returns The resolved {@link PinnedBlock}.
17
+ * @returns The resolved {@link StateBlock}.
19
18
  * @throws {ExternalServiceError} For transport failures, timeouts, or an
20
19
  * aborted signal.
21
20
  * @throws {InvalidSimulationResponseError} When the node returns a block
@@ -8,7 +8,7 @@ interface AssertNoBundlesRetentionParams {
8
8
  /**
9
9
  * Assert that no value is retained by standalone `bundles` periphery contracts
10
10
  * (`VaultExitBundlesV1`, `VaultBundlesV1`, `BlueBundlesV1`) or by
11
- * `MidnightBundlesV1` (`midnightBundles`).
11
+ * `MidnightBundlesV2` (`midnightBundlesV2`).
12
12
  *
13
13
  * Uses net flow (inbound minus outbound) per (restricted address, token) pair.
14
14
  * These contracts legitimately receive tokens as an intermediary (user →
@@ -33,7 +33,7 @@ interface AssertNoBundlesRetentionParams {
33
33
  *
34
34
  * @internal Internal pipeline stage, composed by `simulate`; not part of the
35
35
  * package's public API (only re-exported from the internal pipeline barrel).
36
- * @param params.chainId - Chain whose blue-sdk `bundles` and `midnightBundles`
36
+ * @param params.chainId - Chain whose blue-sdk `bundles` and `midnightBundlesV2`
37
37
  * registry entries define the restricted address set. Chains cataloging
38
38
  * neither are skipped (a `logger.warn` records the skip).
39
39
  * @param params.transfers - Parsed ERC20 / WETH9 transfer flows (plus the
@@ -45,7 +45,7 @@ interface AssertNoBundlesRetentionParams {
45
45
  * @returns Nothing. Returns silently when no restricted contract retains value
46
46
  * above `DUST_THRESHOLD`; otherwise throws.
47
47
  * @throws {BlacklistViolationError} when net inbound flow to a restricted
48
- * `bundles` or `midnightBundles` contract exceeds `DUST_THRESHOLD` for any
48
+ * `bundles` or `midnightBundlesV2` contract exceeds `DUST_THRESHOLD` for any
49
49
  * token.
50
50
  */
51
51
  export declare function assertNoBundlesRetention(params: AssertNoBundlesRetentionParams): void;
@@ -15,7 +15,7 @@ const DUST_THRESHOLD = 100n;
15
15
  /**
16
16
  * Assert that no value is retained by standalone `bundles` periphery contracts
17
17
  * (`VaultExitBundlesV1`, `VaultBundlesV1`, `BlueBundlesV1`) or by
18
- * `MidnightBundlesV1` (`midnightBundles`).
18
+ * `MidnightBundlesV2` (`midnightBundlesV2`).
19
19
  *
20
20
  * Uses net flow (inbound minus outbound) per (restricted address, token) pair.
21
21
  * These contracts legitimately receive tokens as an intermediary (user →
@@ -40,7 +40,7 @@ const DUST_THRESHOLD = 100n;
40
40
  *
41
41
  * @internal Internal pipeline stage, composed by `simulate`; not part of the
42
42
  * package's public API (only re-exported from the internal pipeline barrel).
43
- * @param params.chainId - Chain whose blue-sdk `bundles` and `midnightBundles`
43
+ * @param params.chainId - Chain whose blue-sdk `bundles` and `midnightBundlesV2`
44
44
  * registry entries define the restricted address set. Chains cataloging
45
45
  * neither are skipped (a `logger.warn` records the skip).
46
46
  * @param params.transfers - Parsed ERC20 / WETH9 transfer flows (plus the
@@ -52,12 +52,12 @@ const DUST_THRESHOLD = 100n;
52
52
  * @returns Nothing. Returns silently when no restricted contract retains value
53
53
  * above `DUST_THRESHOLD`; otherwise throws.
54
54
  * @throws {BlacklistViolationError} when net inbound flow to a restricted
55
- * `bundles` or `midnightBundles` contract exceeds `DUST_THRESHOLD` for any
55
+ * `bundles` or `midnightBundlesV2` contract exceeds `DUST_THRESHOLD` for any
56
56
  * token.
57
57
  */
58
58
  export function assertNoBundlesRetention(params) {
59
59
  const { chainId, transfers, assetChanges, logger } = params;
60
- // Standalone bundles contracts and `MidnightBundlesV1` are transient
60
+ // Standalone bundles contracts and `MidnightBundlesV2` are transient
61
61
  // intermediaries that route user value and must never retain it.
62
62
  let addresses;
63
63
  try {
@@ -79,8 +79,8 @@ export function assertNoBundlesRetention(params) {
79
79
  for (const addr of Object.values(addresses.bundles).filter(isDefined))
80
80
  restrictedAddresses.add(getAddress(addr));
81
81
  }
82
- if (addresses.midnightBundles) {
83
- restrictedAddresses.add(getAddress(addresses.midnightBundles));
82
+ if (addresses.midnightBundlesV2) {
83
+ restrictedAddresses.add(getAddress(addresses.midnightBundlesV2));
84
84
  }
85
85
  if (restrictedAddresses.size === 0) {
86
86
  // blue-sdk knows the chain but cataloged no restricted intermediary for it.
@@ -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
  }
@@ -1,7 +1,7 @@
1
1
  import { type Address, type BlockTag, type Hex } from "viem";
2
2
  import type { SimulationAuthorization } from "../../authorizations.js";
3
3
  import type { SimulationLimits } from "../../limits.js";
4
- import { type SimulateParams, type SimulationMode } from "../../params.js";
4
+ import { type SimulateParams, type SimulationMode, type StateBlock } from "../../params.js";
5
5
  /** A normalized user transaction: checksummed addresses, `value` defaulted to `0n`.
6
6
  * @internal
7
7
  */
@@ -21,6 +21,7 @@ export interface ParsedRequest {
21
21
  /** Always empty in final mode. */
22
22
  readonly authorizations: readonly SimulationAuthorization[];
23
23
  readonly blockNumber?: bigint | Exclude<BlockTag, "pending">;
24
+ readonly block?: StateBlock;
24
25
  readonly limits?: SimulationLimits;
25
26
  }
26
27
  /**
@@ -49,6 +49,7 @@ const PERMIT2_MESSAGE_KEYS = PERMIT2_TRANSFER_FIELDS.map((field) => field.name);
49
49
  const PERMIT2_PERMITTED_KEYS = ["token", "amount"];
50
50
  const BLUE_AUTHORIZATION_MESSAGE_KEYS = BLUE_AUTHORIZATION_FIELDS.map((field) => field.name);
51
51
  const TRANSACTION_KEYS = ["from", "to", "data", "value"];
52
+ const BLOCK_KEYS = ["number", "hash", "timestamp"];
52
53
  const LIMITS_KEYS = ["operations"];
53
54
  const AUTHORIZATION_KEYS = {
54
55
  erc20Approval: ["type", "token", "owner", "spender", "amount"],
@@ -567,6 +568,7 @@ export function parseRequest(input) {
567
568
  "chainId",
568
569
  "transactions",
569
570
  "blockNumber",
571
+ "block",
570
572
  "mode",
571
573
  "authorizations",
572
574
  "limits",
@@ -649,6 +651,24 @@ export function parseRequest(input) {
649
651
  }
650
652
  }
651
653
  }
654
+ // block
655
+ const rawBlock = input.block;
656
+ let block;
657
+ if (rawBlock !== undefined) {
658
+ if (!isRecord(rawBlock)) {
659
+ fieldErrors.push("block: must be an object");
660
+ }
661
+ else {
662
+ check.keys(rawBlock, { allow: BLOCK_KEYS, path: "block" });
663
+ const number = check.uint256(readField(rawBlock, "number"), "block.number");
664
+ const hash = check.bytes32(readField(rawBlock, "hash"), "block.hash");
665
+ const timestamp = check.uint256(readField(rawBlock, "timestamp"), "block.timestamp");
666
+ if (number !== undefined && hash !== undefined && timestamp !== undefined)
667
+ block = { number, hash: hash.toLowerCase(), timestamp };
668
+ }
669
+ if (rawBlockNumber !== undefined)
670
+ fieldErrors.push("block: cannot be combined with blockNumber");
671
+ }
652
672
  // authorizations
653
673
  const rawAuthorizations = input.authorizations;
654
674
  const authorizations = [];
@@ -751,6 +771,7 @@ export function parseRequest(input) {
751
771
  transactions,
752
772
  authorizations: mode === "preview" ? authorizations : [],
753
773
  ...(blockNumber !== undefined ? { blockNumber } : {}),
774
+ ...(block !== undefined ? { block } : {}),
754
775
  ...(normalizedLimits !== undefined ? { limits: normalizedLimits } : {}),
755
776
  });
756
777
  }
@@ -4,7 +4,7 @@ import { zeroAddress } from "viem";
4
4
  import { InvalidSimulationResponseError, UnsupportedChainError, } from "../errors.js";
5
5
  import { groupAssetChanges } from "./asset-changes.js";
6
6
  import { createSimulationClient } from "./backends/client.js";
7
- import { assertEndpointChain, executePlan } from "./backends/index.js";
7
+ import { executePlan } from "./backends/index.js";
8
8
  import { resolveAssets } from "./backends/resolve-assets.js";
9
9
  import { resolvePinnedBlock } from "./backends/resolve-pinned-block.js";
10
10
  import { parseTransfers } from "./parsing/index.js";
@@ -42,21 +42,16 @@ export async function runSimulation(params) {
42
42
  if (requiresMorpho && addresses?.blue == null)
43
43
  throw new UnsupportedChainError(request.chainId);
44
44
  const morpho = addresses?.blue ?? zeroAddress;
45
- await assertEndpointChain({
46
- client,
47
- chainId: request.chainId,
48
- mode: request.mode,
49
- blockNumber: typeof request.blockNumber === "bigint" ? request.blockNumber : undefined,
50
- });
51
45
  // Monad nodes reject `validation: false` and charge no gas in the simulated
52
46
  // block either way. Its `latest` block is not final: simulating at its number
53
47
  // runs on another parent, so default to `finalized`.
54
48
  const monad = request.chainId === ChainId.MonadMainnet;
55
- const pinnedBlock = await resolvePinnedBlock({
56
- client,
57
- blockNumber: request.blockNumber ?? (monad ? "finalized" : undefined),
58
- signal,
59
- });
49
+ const pinnedBlock = request.block ??
50
+ (await resolvePinnedBlock({
51
+ client,
52
+ blockNumber: request.blockNumber ?? (monad ? "finalized" : undefined),
53
+ signal,
54
+ }));
60
55
  // With no calldata decoding, the caller's declared `limits.operations`
61
56
  // entries are the operations: subjects for state reads and per-operation
62
57
  // checks come straight from the limit list.
@@ -82,12 +77,14 @@ export async function runSimulation(params) {
82
77
  preparations,
83
78
  reads: observations.reads,
84
79
  });
85
- // Execute against the block resolved once above; the boundary rechecks its hash.
80
+ // Execute against the block resolved once above.
86
81
  const execution = await executePlan({
87
82
  client,
88
83
  plan,
89
84
  stateBlock: pinnedBlock,
90
85
  validation: monad,
86
+ blockGasLimit: chain.blockOverrides?.gasLimit,
87
+ parentHashCheck: chain.parentHashCheck ?? request.chainId !== ChainId.StableMainnet,
91
88
  });
92
89
  const userCalls = execution.calls
93
90
  .filter((call) => call.planned.type === "transaction")