@morpho-org/evm-simulation 5.1.0 → 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 (44) 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 +21 -34
  9. package/lib/cjs/simulate/backends/eth-simulate-v1.js +27 -73
  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.js +1 -1
  13. package/lib/cjs/simulate/backends/resolve-pinned-block.d.ts +6 -12
  14. package/lib/cjs/simulate/backends/resolve-pinned-block.js +3 -4
  15. package/lib/cjs/simulate/pipeline/bundles-retention.d.ts +3 -3
  16. package/lib/cjs/simulate/pipeline/bundles-retention.js +6 -6
  17. package/lib/cjs/simulate/request/parse-request.d.ts +2 -1
  18. package/lib/cjs/simulate/request/parse-request.js +21 -0
  19. package/lib/cjs/simulate/run-simulation.js +7 -12
  20. package/lib/cjs/simulate/simulate.d.ts +25 -17
  21. package/lib/cjs/simulate/simulate.js +25 -17
  22. package/lib/cjs/types.d.ts +1 -1
  23. package/lib/esm/errors.d.ts +17 -2
  24. package/lib/esm/errors.js +17 -2
  25. package/lib/esm/index.d.ts +2 -2
  26. package/lib/esm/index.js +1 -1
  27. package/lib/esm/params.d.ts +20 -1
  28. package/lib/esm/requirements/requirements.js +2 -1
  29. package/lib/esm/simulate/backends/eth-simulate-v1.d.ts +21 -34
  30. package/lib/esm/simulate/backends/eth-simulate-v1.js +29 -74
  31. package/lib/esm/simulate/backends/index.d.ts +1 -1
  32. package/lib/esm/simulate/backends/index.js +1 -1
  33. package/lib/esm/simulate/backends/parse-response.js +1 -1
  34. package/lib/esm/simulate/backends/resolve-pinned-block.d.ts +6 -12
  35. package/lib/esm/simulate/backends/resolve-pinned-block.js +3 -4
  36. package/lib/esm/simulate/pipeline/bundles-retention.d.ts +3 -3
  37. package/lib/esm/simulate/pipeline/bundles-retention.js +6 -6
  38. package/lib/esm/simulate/request/parse-request.d.ts +2 -1
  39. package/lib/esm/simulate/request/parse-request.js +21 -0
  40. package/lib/esm/simulate/run-simulation.js +8 -13
  41. package/lib/esm/simulate/simulate.d.ts +25 -17
  42. package/lib/esm/simulate/simulate.js +25 -17
  43. package/lib/esm/types.d.ts +1 -1
  44. package/package.json +4 -4
@@ -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,38 +61,15 @@ 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
75
  * gas is separated from economic effects. When `blockGasLimit` is set,
@@ -92,32 +78,31 @@ export async function assertEndpointChain(params) {
92
78
  * **No balance override is
93
79
  * applied** — `value` transfers are funded by the sender's real native
94
80
  * balance.
95
- * 2. **Response validation** — the response is parsed before the reorg
96
- * re-fetch so revert/mismatch evidence already in hand surfaces instead
97
- * of being downgraded to a bypassable {@link ExternalServiceError} by a
98
- * failing re-fetch. The simulated block must be exactly
81
+ * 2. **Response validation** — the simulated block must be exactly
99
82
  * `stateBlockNumber` or `stateBlockNumber + 1`: geth-style nodes report
100
83
  * the former's successor while Anvil reports the pinned block itself.
101
84
  * The result records whatever the node returns; consumers must read
102
85
  * {@link ExecutionBlock.blockNumber} and never assume +1.
103
- * 3. **Reorg check** — the pinned state block is re-fetched last to detect a reorg
104
- * that swapped its hash mid-flight (`InvalidSimulationResponseError`).
105
86
  *
106
87
  * The endpoint must support `eth_simulateV1` with per-call `from`; there is
107
88
  * no fallback backend.
108
89
  *
109
- * @param params - Shared simulation client, execution plan, already-pinned
110
- * state block, the `validation` flag to send, the optional simulated
111
- * block gas limit, and whether to check the successor `parentHash`.
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`.
112
94
  * @returns Deep-frozen {@link SimulationExecution} — per-transaction call
113
95
  * results and the resolved {@link ExecutionBlock}.
114
96
  * @throws {ExternalServiceError} For transport failures, timeouts,
115
97
  * or malformed JSON-RPC envelopes.
116
98
  * @throws {InvalidSimulationResponseError} For a response that cannot be
117
- * trusted (bad shape, call-count mismatch, block
118
- * other than the pinned state block or its successor, a block timestamp
119
- * earlier than the pinned block's, a per-call result that fails normalization,
120
- * 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).
121
106
  * @throws {MissingVerificationEvidenceError} When a planned state read fails.
122
107
  * @throws {SimulationRevertedError} When a preparation or user transaction
123
108
  * reverts or the node reports a bundle-level revert (code 3 / insufficient
@@ -136,6 +121,7 @@ export async function executePlan(params) {
136
121
  blockOverrides: { gasLimit: numberToHex(blockGasLimit) },
137
122
  }),
138
123
  calls: plan.calls.map((call) => ({
124
+ chainId: numberToHex(plan.request.chainId),
139
125
  from: call.transaction.from,
140
126
  to: call.transaction.to,
141
127
  data: call.transaction.data,
@@ -149,12 +135,7 @@ export async function executePlan(params) {
149
135
  numberToHex(stateBlock.number),
150
136
  ],
151
137
  }));
152
- // Response parsing is validation, not transport — it runs before the
153
- // reorg re-fetch so evidence already in hand reaches the caller as
154
- // InvalidSimulationResponseError / SimulationRevertedError instead of
155
- // being downgraded to a bypassable ExternalServiceError by a failing
156
- // re-fetch.
157
- const execution = parseSimulationResponse({
138
+ return parseSimulationResponse({
158
139
  plan,
159
140
  blocks: response,
160
141
  stateBlockNumber: stateBlock.number,
@@ -162,30 +143,4 @@ export async function executePlan(params) {
162
143
  stateBlockTimestamp: stateBlock.timestamp,
163
144
  parentHashCheck,
164
145
  });
165
- // Reorg window: the pinned state block must still carry the same hash
166
- // after simulation, or the result may describe a different chain tip.
167
- const stateBlockAfter = await rpc("eth_getBlock", () => client.getBlock({ blockNumber: stateBlock.number })).catch((error) => {
168
- if (error instanceof ExternalServiceError &&
169
- error.cause instanceof BlockNotFoundError)
170
- return error;
171
- throw error;
172
- });
173
- if (stateBlockAfter instanceof ExternalServiceError ||
174
- stateBlockAfter.hash !== stateBlock.hash) {
175
- const after = stateBlockAfter instanceof ExternalServiceError
176
- ? "unavailable"
177
- : stateBlockAfter.hash;
178
- throw new InvalidSimulationResponseError(`State block ${stateBlock.number} hash changed during simulation (reorg): ${stateBlock.hash} became ${after}. Re-submit the simulation.`, {
179
- context: {
180
- stage: "transport",
181
- chainId: plan.request.chainId,
182
- mode: plan.request.mode,
183
- blockNumber: stateBlock.number,
184
- },
185
- ...(stateBlockAfter instanceof ExternalServiceError && {
186
- cause: stateBlockAfter.cause,
187
- }),
188
- });
189
- }
190
- return execution;
191
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";
@@ -55,7 +55,7 @@ export function parseSimulationResponse(params) {
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
57
  // the pinned state block hash. With `parentHashCheck` off (Stable by
58
- // default) the block number, timestamp and reorg checks still apply.
58
+ // default) the block number and timestamp checks still apply.
59
59
  if (params.parentHashCheck &&
60
60
  blockNumber === params.stateBlockNumber + 1n &&
61
61
  block.parentHash !== params.stateBlockHash) {
@@ -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.
@@ -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,7 +77,7 @@ 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,
@@ -4,11 +4,11 @@ import type { SimulationConfig } from "../types.js";
4
4
  /**
5
5
  * Simulate a bundle of EVM transactions.
6
6
  *
7
- * Parses and normalizes the request → resolves the chain identity → resolves
8
- * a single pinned block → resolves quoted-asset metadata → plans state reads
9
- * and the execution → executes once through `eth_simulateV1` under the full
10
- * timeout budget (simulation with in-block state reads → response parsing →
11
- * reorg check) → derives ERC20/WETH9 transfers and net asset changes from the
7
+ * Parses and normalizes the request → resolves a single pinned block (skipped
8
+ * when `params.block` is supplied) → resolves quoted-asset metadata → plans
9
+ * state reads and the execution → executes once through `eth_simulateV1` under
10
+ * the full timeout budget (simulation with in-block state reads → response
11
+ * parsing) → derives ERC20/WETH9 transfers and net asset changes from the
12
12
  * user calls only → decodes quoted balances/positions → runs slippage checks
13
13
  * → asserts no funds are retained by standalone `bundles` periphery contracts
14
14
  * → returns the result. The caller reads whichever fields they need:
@@ -41,7 +41,9 @@ import type { SimulationConfig } from "../types.js";
41
41
  * `blockOverrides.gasLimit` and `parentHashCheck`, optional logger, and the
42
42
  * overall timeout budget.
43
43
  * @param params - Per-call simulation input.
44
- * @param params.chainId - Chain id the bundle targets; must match the endpoint.
44
+ * @param params.chainId - Chain id the bundle targets. Sent on every call inside
45
+ * `eth_simulateV1`, so nodes that check it reject a wrong-chain endpoint;
46
+ * there is no separate `eth_chainId` lookup.
45
47
  * @param params.transactions - The bundle's transactions, in execution order.
46
48
  * All must share the same `from`.
47
49
  * @param params.mode - `"final"` (default) or `"preview"`.
@@ -51,9 +53,14 @@ import type { SimulationConfig } from "../types.js";
51
53
  * than `"pending"`.
52
54
  * Defaults to `latest` (`finalized` on Monad, whose `latest` block is not
53
55
  * final), resolved exactly once.
56
+ * @param params.block - Optional caller-supplied state block (`number`,
57
+ * `hash`, `timestamp`). Skips the block lookup, so a simulation without
58
+ * asset metadata reads makes a single `eth_simulateV1` request. Cannot be
59
+ * combined with `blockNumber`.
54
60
  * @throws {SimulationValidationError} for invalid input (mixed senders, bad
55
61
  * addresses, empty transactions, malformed authorizations, final-mode
56
- * authorizations, malformed limits, unknown fields, a `"pending"` block tag, a
62
+ * authorizations, malformed limits, unknown fields, a `"pending"` block tag,
63
+ * a malformed `block` or one combined with `blockNumber`, a
57
64
  * `blockOverrides.gasLimit` that is not a positive bigint, a `parentHashCheck`
58
65
  * that is not a boolean, or share quotes for
59
66
  * `blueSupplyCollateral` / `blueWithdrawCollateral`).
@@ -69,20 +76,21 @@ import type { SimulationConfig } from "../types.js";
69
76
  * @throws {InvalidSimulationResponseError} when the node response cannot be
70
77
  * trusted (bad shape, call-count mismatch, block that is neither the pinned
71
78
  * state block nor its immediate successor, a successor with a mismatched
72
- * `parentHash` (unless `parentHashCheck` is off; off by default on Stable, chain 988), a block timestamp earlier than the pinned block's, a
73
- * malformed per-call result, a quoted balance/position read whose non-empty
74
- * return data cannot be decoded, a state-block hash that changed or a pinned
75
- * block that vanished mid-flight, or an
76
- * endpoint whose `eth_chainId` differs from `params.chainId`; chain identity
77
- * is checked before any block lookup).
79
+ * `parentHash` (unless `parentHashCheck` is off; off by default on Stable,
80
+ * chain 988), a block timestamp earlier than the pinned block's, a
81
+ * malformed per-call result, or a quoted balance/position read whose
82
+ * non-empty return data cannot be decoded).
83
+ * @throws {InvalidChainIdError} when the node rejects the request `chainId`
84
+ * because the endpoint serves another chain (a subclass of
85
+ * `InvalidSimulationResponseError` sharing its `name`/`code`;
86
+ * `instanceof InvalidChainIdError` tells them apart). Never bypassable.
78
87
  * @throws {BlacklistViolationError} when the simulation leaves value retained
79
88
  * beyond the dust threshold by a `bundles` periphery contract
80
- * (VaultExitBundlesV1, VaultBundlesV1, BlueBundlesV1, MidnightBundlesV1).
89
+ * (VaultExitBundlesV1, VaultBundlesV1, BlueBundlesV1, MidnightBundlesV2).
81
90
  * Never bypassable.
82
91
  * @throws {ExternalServiceError} when the RPC is unavailable within the
83
- * timeout budget or returns a malformed JSON-RPC envelope. Chain-id
84
- * mismatches and a state block without number/hash are reported as
85
- * `InvalidSimulationResponseError`.
92
+ * timeout budget or returns a malformed JSON-RPC envelope. A looked-up state
93
+ * block without number/hash is reported as `InvalidSimulationResponseError`.
86
94
  * @returns A frozen {@link VerifiedSimulationResult} carrying the normalized
87
95
  * `simulationTxs`, per-tx `calls` (aligned 1:1), parsed `transfers` (each
88
96
  * stamped with `txIdx`), per-account net `assetChanges`, and per-operation
@@ -3,11 +3,11 @@ import { runSimulation } from "./run-simulation.js";
3
3
  /**
4
4
  * Simulate a bundle of EVM transactions.
5
5
  *
6
- * Parses and normalizes the request → resolves the chain identity → resolves
7
- * a single pinned block → resolves quoted-asset metadata → plans state reads
8
- * and the execution → executes once through `eth_simulateV1` under the full
9
- * timeout budget (simulation with in-block state reads → response parsing →
10
- * reorg check) → derives ERC20/WETH9 transfers and net asset changes from the
6
+ * Parses and normalizes the request → resolves a single pinned block (skipped
7
+ * when `params.block` is supplied) → resolves quoted-asset metadata → plans
8
+ * state reads and the execution → executes once through `eth_simulateV1` under
9
+ * the full timeout budget (simulation with in-block state reads → response
10
+ * parsing) → derives ERC20/WETH9 transfers and net asset changes from the
11
11
  * user calls only → decodes quoted balances/positions → runs slippage checks
12
12
  * → asserts no funds are retained by standalone `bundles` periphery contracts
13
13
  * → returns the result. The caller reads whichever fields they need:
@@ -40,7 +40,9 @@ import { runSimulation } from "./run-simulation.js";
40
40
  * `blockOverrides.gasLimit` and `parentHashCheck`, optional logger, and the
41
41
  * overall timeout budget.
42
42
  * @param params - Per-call simulation input.
43
- * @param params.chainId - Chain id the bundle targets; must match the endpoint.
43
+ * @param params.chainId - Chain id the bundle targets. Sent on every call inside
44
+ * `eth_simulateV1`, so nodes that check it reject a wrong-chain endpoint;
45
+ * there is no separate `eth_chainId` lookup.
44
46
  * @param params.transactions - The bundle's transactions, in execution order.
45
47
  * All must share the same `from`.
46
48
  * @param params.mode - `"final"` (default) or `"preview"`.
@@ -50,9 +52,14 @@ import { runSimulation } from "./run-simulation.js";
50
52
  * than `"pending"`.
51
53
  * Defaults to `latest` (`finalized` on Monad, whose `latest` block is not
52
54
  * final), resolved exactly once.
55
+ * @param params.block - Optional caller-supplied state block (`number`,
56
+ * `hash`, `timestamp`). Skips the block lookup, so a simulation without
57
+ * asset metadata reads makes a single `eth_simulateV1` request. Cannot be
58
+ * combined with `blockNumber`.
53
59
  * @throws {SimulationValidationError} for invalid input (mixed senders, bad
54
60
  * addresses, empty transactions, malformed authorizations, final-mode
55
- * authorizations, malformed limits, unknown fields, a `"pending"` block tag, a
61
+ * authorizations, malformed limits, unknown fields, a `"pending"` block tag,
62
+ * a malformed `block` or one combined with `blockNumber`, a
56
63
  * `blockOverrides.gasLimit` that is not a positive bigint, a `parentHashCheck`
57
64
  * that is not a boolean, or share quotes for
58
65
  * `blueSupplyCollateral` / `blueWithdrawCollateral`).
@@ -68,20 +75,21 @@ import { runSimulation } from "./run-simulation.js";
68
75
  * @throws {InvalidSimulationResponseError} when the node response cannot be
69
76
  * trusted (bad shape, call-count mismatch, block that is neither the pinned
70
77
  * state block nor its immediate successor, a successor with a mismatched
71
- * `parentHash` (unless `parentHashCheck` is off; off by default on Stable, chain 988), a block timestamp earlier than the pinned block's, a
72
- * malformed per-call result, a quoted balance/position read whose non-empty
73
- * return data cannot be decoded, a state-block hash that changed or a pinned
74
- * block that vanished mid-flight, or an
75
- * endpoint whose `eth_chainId` differs from `params.chainId`; chain identity
76
- * is checked before any block lookup).
78
+ * `parentHash` (unless `parentHashCheck` is off; off by default on Stable,
79
+ * chain 988), a block timestamp earlier than the pinned block's, a
80
+ * malformed per-call result, or a quoted balance/position read whose
81
+ * non-empty return data cannot be decoded).
82
+ * @throws {InvalidChainIdError} when the node rejects the request `chainId`
83
+ * because the endpoint serves another chain (a subclass of
84
+ * `InvalidSimulationResponseError` sharing its `name`/`code`;
85
+ * `instanceof InvalidChainIdError` tells them apart). Never bypassable.
77
86
  * @throws {BlacklistViolationError} when the simulation leaves value retained
78
87
  * beyond the dust threshold by a `bundles` periphery contract
79
- * (VaultExitBundlesV1, VaultBundlesV1, BlueBundlesV1, MidnightBundlesV1).
88
+ * (VaultExitBundlesV1, VaultBundlesV1, BlueBundlesV1, MidnightBundlesV2).
80
89
  * Never bypassable.
81
90
  * @throws {ExternalServiceError} when the RPC is unavailable within the
82
- * timeout budget or returns a malformed JSON-RPC envelope. Chain-id
83
- * mismatches and a state block without number/hash are reported as
84
- * `InvalidSimulationResponseError`.
91
+ * timeout budget or returns a malformed JSON-RPC envelope. A looked-up state
92
+ * block without number/hash is reported as `InvalidSimulationResponseError`.
85
93
  * @returns A frozen {@link VerifiedSimulationResult} carrying the normalized
86
94
  * `simulationTxs`, per-tx `calls` (aligned 1:1), parsed `transfers` (each
87
95
  * stamped with `txIdx`), per-account net `assetChanges`, and per-operation
@@ -13,7 +13,7 @@ export interface ChainSimulationConfig {
13
13
  };
14
14
  /**
15
15
  * Whether a simulated successor block's `parentHash` must equal the pinned
16
- * state block hash. Block number, timestamp and reorg checks always run.
16
+ * state block hash. Block number and timestamp checks always run.
17
17
  * Omitted: on everywhere except Stable (988), whose nodes report a
18
18
  * `parentHash` that never matches the pinned block.
19
19
  */
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.1.0",
4
+ "version": "6.0.0",
5
5
  "author": "Morpho Association <contact@morpho.org>",
6
6
  "repository": {
7
7
  "type": "git",
@@ -21,9 +21,9 @@
21
21
  "lib"
22
22
  ],
23
23
  "dependencies": {
24
- "@morpho-org/blue-sdk": "^7.2.0",
25
- "@morpho-org/morpho-sdk": "^6.6.0",
26
- "@morpho-org/morpho-ts": "^3.2.1"
24
+ "@morpho-org/blue-sdk": "^8.0.0",
25
+ "@morpho-org/morpho-sdk": "^7.0.0",
26
+ "@morpho-org/morpho-ts": "^4.0.0"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "viem": "^2.0.0"