@morpho-org/evm-simulation 4.2.0 → 5.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 (160) hide show
  1. package/README.md +109 -17
  2. package/lib/cjs/authorizations.d.ts +95 -0
  3. package/lib/cjs/authorizations.js +2 -0
  4. package/lib/cjs/errors.d.ts +207 -13
  5. package/lib/cjs/errors.js +197 -13
  6. package/lib/cjs/index.d.ts +10 -2
  7. package/lib/cjs/index.js +19 -3
  8. package/lib/cjs/limits.d.ts +112 -0
  9. package/lib/cjs/limits.js +50 -0
  10. package/lib/cjs/params.d.ts +25 -0
  11. package/lib/cjs/params.js +5 -0
  12. package/lib/cjs/requirements/index.d.ts +1 -0
  13. package/lib/cjs/requirements/index.js +5 -0
  14. package/lib/cjs/requirements/requirements.d.ts +64 -0
  15. package/lib/cjs/requirements/requirements.js +149 -0
  16. package/lib/cjs/result.d.ts +49 -0
  17. package/lib/cjs/result.js +2 -0
  18. package/lib/cjs/simulate/asset-changes.d.ts +7 -5
  19. package/lib/cjs/simulate/asset-changes.js +6 -5
  20. package/lib/cjs/simulate/backends/client.d.ts +16 -0
  21. package/lib/cjs/simulate/backends/client.js +27 -0
  22. package/lib/cjs/simulate/backends/eth-simulate-v1.d.ts +66 -20
  23. package/lib/cjs/simulate/backends/eth-simulate-v1.js +172 -98
  24. package/lib/cjs/simulate/backends/index.d.ts +1 -2
  25. package/lib/cjs/simulate/backends/index.js +3 -4
  26. package/lib/cjs/simulate/backends/parse-response.d.ts +69 -0
  27. package/lib/cjs/simulate/backends/parse-response.js +168 -0
  28. package/lib/cjs/simulate/backends/resolve-assets.d.ts +26 -0
  29. package/lib/cjs/simulate/backends/resolve-assets.js +111 -0
  30. package/lib/cjs/simulate/backends/resolve-pinned-block.d.ts +33 -0
  31. package/lib/cjs/simulate/backends/resolve-pinned-block.js +46 -0
  32. package/lib/cjs/simulate/context.d.ts +14 -0
  33. package/lib/cjs/simulate/context.js +2 -0
  34. package/lib/cjs/simulate/measurement-plan.d.ts +29 -0
  35. package/lib/cjs/simulate/measurement-plan.js +115 -0
  36. package/lib/cjs/simulate/parsing/transfers.d.ts +4 -3
  37. package/lib/cjs/simulate/parsing/transfers.js +4 -6
  38. package/lib/cjs/simulate/pipeline/bundles-retention.d.ts +7 -10
  39. package/lib/cjs/simulate/pipeline/bundles-retention.js +11 -14
  40. package/lib/cjs/simulate/pipeline/resolve-chain.d.ts +5 -11
  41. package/lib/cjs/simulate/pipeline/resolve-chain.js +5 -10
  42. package/lib/cjs/simulate/plan/plan-execution.d.ts +58 -0
  43. package/lib/cjs/simulate/plan/plan-execution.js +75 -0
  44. package/lib/cjs/simulate/prepare.d.ts +30 -0
  45. package/lib/cjs/simulate/prepare.js +80 -0
  46. package/lib/cjs/simulate/request/index.d.ts +1 -0
  47. package/lib/cjs/simulate/request/index.js +5 -0
  48. package/lib/cjs/simulate/request/parse-request.d.ts +44 -0
  49. package/lib/cjs/simulate/request/parse-request.js +759 -0
  50. package/lib/cjs/simulate/result.d.ts +19 -0
  51. package/lib/cjs/simulate/result.js +29 -0
  52. package/lib/cjs/simulate/run-simulation.d.ts +18 -0
  53. package/lib/cjs/simulate/run-simulation.js +151 -0
  54. package/lib/cjs/simulate/simulate.d.ts +100 -50
  55. package/lib/cjs/simulate/simulate.js +99 -81
  56. package/lib/cjs/simulate/slippage.d.ts +18 -0
  57. package/lib/cjs/simulate/slippage.js +41 -0
  58. package/lib/cjs/simulate/state/contract.d.ts +19 -0
  59. package/lib/cjs/simulate/state/contract.js +2 -0
  60. package/lib/cjs/simulate/state/read-state.d.ts +60 -0
  61. package/lib/cjs/simulate/state/read-state.js +148 -0
  62. package/lib/cjs/simulate/verify-slippage.d.ts +24 -0
  63. package/lib/cjs/simulate/verify-slippage.js +85 -0
  64. package/lib/cjs/types.d.ts +35 -95
  65. package/lib/esm/authorizations.d.ts +95 -0
  66. package/lib/esm/authorizations.js +1 -0
  67. package/lib/esm/errors.d.ts +207 -13
  68. package/lib/esm/errors.js +189 -12
  69. package/lib/esm/index.d.ts +10 -2
  70. package/lib/esm/index.js +4 -1
  71. package/lib/esm/limits.d.ts +112 -0
  72. package/lib/esm/limits.js +47 -0
  73. package/lib/esm/params.d.ts +25 -0
  74. package/lib/esm/params.js +2 -0
  75. package/lib/esm/requirements/index.d.ts +1 -0
  76. package/lib/esm/requirements/index.js +1 -0
  77. package/lib/esm/requirements/requirements.d.ts +64 -0
  78. package/lib/esm/requirements/requirements.js +146 -0
  79. package/lib/esm/result.d.ts +49 -0
  80. package/lib/esm/result.js +1 -0
  81. package/lib/esm/simulate/asset-changes.d.ts +7 -5
  82. package/lib/esm/simulate/asset-changes.js +6 -5
  83. package/lib/esm/simulate/backends/client.d.ts +16 -0
  84. package/lib/esm/simulate/backends/client.js +24 -0
  85. package/lib/esm/simulate/backends/eth-simulate-v1.d.ts +66 -20
  86. package/lib/esm/simulate/backends/eth-simulate-v1.js +170 -99
  87. package/lib/esm/simulate/backends/index.d.ts +1 -2
  88. package/lib/esm/simulate/backends/index.js +1 -2
  89. package/lib/esm/simulate/backends/parse-response.d.ts +69 -0
  90. package/lib/esm/simulate/backends/parse-response.js +165 -0
  91. package/lib/esm/simulate/backends/resolve-assets.d.ts +26 -0
  92. package/lib/esm/simulate/backends/resolve-assets.js +108 -0
  93. package/lib/esm/simulate/backends/resolve-pinned-block.d.ts +33 -0
  94. package/lib/esm/simulate/backends/resolve-pinned-block.js +43 -0
  95. package/lib/esm/simulate/context.d.ts +14 -0
  96. package/lib/esm/simulate/context.js +1 -0
  97. package/lib/esm/simulate/measurement-plan.d.ts +29 -0
  98. package/lib/esm/simulate/measurement-plan.js +112 -0
  99. package/lib/esm/simulate/parsing/transfers.d.ts +4 -3
  100. package/lib/esm/simulate/parsing/transfers.js +4 -6
  101. package/lib/esm/simulate/pipeline/bundles-retention.d.ts +7 -10
  102. package/lib/esm/simulate/pipeline/bundles-retention.js +11 -14
  103. package/lib/esm/simulate/pipeline/resolve-chain.d.ts +5 -11
  104. package/lib/esm/simulate/pipeline/resolve-chain.js +5 -10
  105. package/lib/esm/simulate/plan/plan-execution.d.ts +58 -0
  106. package/lib/esm/simulate/plan/plan-execution.js +72 -0
  107. package/lib/esm/simulate/prepare.d.ts +30 -0
  108. package/lib/esm/simulate/prepare.js +77 -0
  109. package/lib/esm/simulate/request/index.d.ts +1 -0
  110. package/lib/esm/simulate/request/index.js +1 -0
  111. package/lib/esm/simulate/request/parse-request.d.ts +44 -0
  112. package/lib/esm/simulate/request/parse-request.js +756 -0
  113. package/lib/esm/simulate/result.d.ts +19 -0
  114. package/lib/esm/simulate/result.js +26 -0
  115. package/lib/esm/simulate/run-simulation.d.ts +18 -0
  116. package/lib/esm/simulate/run-simulation.js +148 -0
  117. package/lib/esm/simulate/simulate.d.ts +100 -50
  118. package/lib/esm/simulate/simulate.js +99 -81
  119. package/lib/esm/simulate/slippage.d.ts +18 -0
  120. package/lib/esm/simulate/slippage.js +38 -0
  121. package/lib/esm/simulate/state/contract.d.ts +19 -0
  122. package/lib/esm/simulate/state/contract.js +1 -0
  123. package/lib/esm/simulate/state/read-state.d.ts +60 -0
  124. package/lib/esm/simulate/state/read-state.js +144 -0
  125. package/lib/esm/simulate/verify-slippage.d.ts +24 -0
  126. package/lib/esm/simulate/verify-slippage.js +82 -0
  127. package/lib/esm/types.d.ts +35 -95
  128. package/package.json +7 -6
  129. package/lib/cjs/simulate/authorizations/index.d.ts +0 -2
  130. package/lib/cjs/simulate/authorizations/index.js +0 -7
  131. package/lib/cjs/simulate/authorizations/resolve.d.ts +0 -9
  132. package/lib/cjs/simulate/authorizations/resolve.js +0 -29
  133. package/lib/cjs/simulate/authorizations/validate.d.ts +0 -6
  134. package/lib/cjs/simulate/authorizations/validate.js +0 -23
  135. package/lib/cjs/simulate/backends/tenderly-rpc.d.ts +0 -23
  136. package/lib/cjs/simulate/backends/tenderly-rpc.js +0 -234
  137. package/lib/cjs/simulate/pipeline/build-simulation-txs.d.ts +0 -17
  138. package/lib/cjs/simulate/pipeline/build-simulation-txs.js +0 -50
  139. package/lib/cjs/simulate/pipeline/execute-simulation.d.ts +0 -22
  140. package/lib/cjs/simulate/pipeline/execute-simulation.js +0 -82
  141. package/lib/cjs/simulate/pipeline/index.d.ts +0 -4
  142. package/lib/cjs/simulate/pipeline/index.js +0 -11
  143. package/lib/cjs/simulate/pipeline/validate-input.d.ts +0 -10
  144. package/lib/cjs/simulate/pipeline/validate-input.js +0 -55
  145. package/lib/esm/simulate/authorizations/index.d.ts +0 -2
  146. package/lib/esm/simulate/authorizations/index.js +0 -2
  147. package/lib/esm/simulate/authorizations/resolve.d.ts +0 -9
  148. package/lib/esm/simulate/authorizations/resolve.js +0 -26
  149. package/lib/esm/simulate/authorizations/validate.d.ts +0 -6
  150. package/lib/esm/simulate/authorizations/validate.js +0 -20
  151. package/lib/esm/simulate/backends/tenderly-rpc.d.ts +0 -23
  152. package/lib/esm/simulate/backends/tenderly-rpc.js +0 -231
  153. package/lib/esm/simulate/pipeline/build-simulation-txs.d.ts +0 -17
  154. package/lib/esm/simulate/pipeline/build-simulation-txs.js +0 -47
  155. package/lib/esm/simulate/pipeline/execute-simulation.d.ts +0 -22
  156. package/lib/esm/simulate/pipeline/execute-simulation.js +0 -79
  157. package/lib/esm/simulate/pipeline/index.d.ts +0 -4
  158. package/lib/esm/simulate/pipeline/index.js +0 -4
  159. package/lib/esm/simulate/pipeline/validate-input.d.ts +0 -10
  160. package/lib/esm/simulate/pipeline/validate-input.js +0 -52
@@ -1,111 +1,182 @@
1
- import { createPublicClient, ExecutionRevertedError, getAddress, http, maxUint256, } from "viem";
2
- import { ExternalServiceError, SimulationRevertedError, SimulationValidationError, } from "../../errors.js";
3
- import { groupAssetChanges } from "../asset-changes.js";
4
- import { parseTransfers } from "../parsing/index.js";
1
+ import { BaseError, BlockNotFoundError, ExecutionRevertedError, InsufficientFundsError, numberToHex, } from "viem";
2
+ import { ExternalServiceError, InvalidSimulationResponseError, SimulationPackageError, SimulationRevertedError, } from "../../errors.js";
3
+ import { parseSimulationResponse } from "./parse-response.js";
5
4
  /**
6
- * Simulate transactions using eth_simulateV1 (viem simulateCalls).
7
- *
8
- * Uses stateOverride to set the user's ETH balance high enough to avoid
9
- * false "insufficient balance for gas" reverts.
10
- *
11
- * Runs with `traceTransfers` enabled, so the node synthesizes native-ETH moves
12
- * (the top-level `value` *and* ETH moved through internal calls — e.g. a
13
- * `WETH.withdraw` refund or a swap that pays out ETH) as `Transfer` events from
14
- * the native sentinel. Derives `assetChanges` (net per-token deltas grouped by
15
- * account) entirely from the emitted transfer logs, with native ETH normalized
16
- * to viem's `ethAddress`. Top-level `value` is intentionally *not* added on top:
17
- * `traceTransfers` already logs it, so adding it would double-count.
5
+ * Classify a node-level revert: viem's `ExecutionRevertedError`, or a raw
6
+ * JSON-RPC error — code 3 for "execution reverted", plus an "insufficient
7
+ * funds" message for an unfundable `value` transfer under real native
8
+ * funding (the numeric code varies by node, so the message is matched).
18
9
  */
19
- export async function simulateV1(params) {
20
- const { rpcUrl, transactions, blockNumber, wNative, signal } = params;
21
- const client = createPublicClient({
22
- transport: http(rpcUrl, {
23
- fetchOptions: signal ? { signal } : undefined,
24
- }),
25
- });
26
- const firstTx = transactions[0];
27
- if (!firstTx) {
28
- throw new SimulationValidationError("At least one transaction is required", []);
29
- }
30
- const sender = getAddress(firstTx.from);
31
- // eth_simulateV1 executes all calls as the same account. Reject mixed senders
32
- // to avoid silently simulating under the wrong address.
33
- const mixedSender = transactions.find((tx) => getAddress(tx.from) !== sender);
34
- if (mixedSender) {
35
- throw new SimulationValidationError("All transactions must have the same from address for eth_simulateV1", [`expected ${sender}, got ${mixedSender.from}`]);
10
+ const isNodeRevert = (error) => error instanceof ExecutionRevertedError ||
11
+ error instanceof InsufficientFundsError ||
12
+ (error instanceof Error &&
13
+ (("code" in error && error.code === 3) ||
14
+ /insufficient funds/i.test(error.message)));
15
+ /**
16
+ * Trim a caught error to a safe message: viem's `shortMessage` drops the
17
+ * URL/request-body details its `message` embeds. The original error is always
18
+ * kept as `cause` for logging.
19
+ */
20
+ const safeMessage = (error) => error instanceof BaseError ? error.shortMessage : String(error);
21
+ /** Map a caught error to the boundary's typed failure for `label`. */
22
+ const toBoundaryError = (label, error) => {
23
+ if (error instanceof SimulationPackageError)
24
+ return error;
25
+ // A node-level revert is a property of the bundle, not the backend. The
26
+ // execution-stage context requires an operation-keyed subject and a
27
+ // node-level revert precedes operation decoding, so no context attaches.
28
+ // `details` carries only the URL-free code/shortMessage; the raw viem
29
+ // error (which embeds the RPC URL) is kept as `cause` only.
30
+ if (label === "eth_simulateV1" && isNodeRevert(error)) {
31
+ const reverted = new SimulationRevertedError(error instanceof BaseError
32
+ ? error.details || error.shortMessage
33
+ : error.message, {
34
+ code: "code" in error && typeof error.code !== "undefined"
35
+ ? error.code
36
+ : undefined,
37
+ shortMessage: error instanceof BaseError ? error.shortMessage : error.message,
38
+ }, "UNKNOWN_REVERT");
39
+ reverted.cause = error;
40
+ return reverted;
36
41
  }
37
- const calls = transactions.map((tx) => ({
38
- to: tx.to,
39
- data: tx.data,
40
- value: tx.value,
41
- }));
42
- const blockParam = typeof blockNumber === "bigint"
43
- ? { blockNumber }
44
- : blockNumber !== undefined
45
- ? { blockTag: blockNumber }
46
- : {};
42
+ return new ExternalServiceError(`${label} error: ${safeMessage(error)}`, {
43
+ cause: error,
44
+ });
45
+ };
46
+ /** Run one RPC call; anything thrown becomes a typed boundary error. @internal */
47
+ export const rpc = async (label, call) => {
47
48
  try {
48
- const simulationResult = await client.simulateCalls({
49
- account: sender,
50
- calls,
51
- ...blockParam,
52
- // Synthesize native-ETH moves (top-level value + internal calls) as
53
- // Transfer logs from the native sentinel, so `parseTransfers` captures
54
- // ETH that emits no real log (e.g. a WETH.withdraw refund).
55
- traceTransfers: true,
56
- // Inflate sender ETH balance to prevent false "insufficient gas" reverts.
57
- // Without this, valid ERC20 flows fail when the sender has low ETH.
58
- // Use half of uint256 (not the ceiling) so the override leaves headroom
59
- // for inbound native ETH — e.g. a WETH.withdraw refund or a swap payout
60
- // would overflow the recipient balance and revert the value transfer if
61
- // the sender were pinned at maxUint256.
62
- stateOverrides: [{ address: sender, balance: maxUint256 / 2n }],
63
- });
64
- const results = simulationResult.results;
65
- if (!Array.isArray(results)) {
66
- throw new ExternalServiceError("eth_simulateV1 returned unexpected response format");
67
- }
68
- const failedResult = results.find((r) => r.status !== "success");
69
- if (failedResult) {
70
- throw new SimulationRevertedError(failedResult.error?.message ?? "Simulation failed", results);
71
- }
72
- const rawCalls = results.map((r) => ({
73
- logs: (r.logs ?? []).map((log) => ({
74
- address: log.address,
75
- topics: [...log.topics],
76
- data: log.data ?? "0x",
77
- })),
78
- status: r.status === "success",
79
- returnData: r.data ?? "0x",
80
- gasUsed: r.gasUsed,
81
- }));
82
- return {
83
- calls: rawCalls,
84
- assetChanges: toAssetChanges(parseTransfers(rawCalls, { wNative })),
85
- };
49
+ return await call();
86
50
  }
87
51
  catch (error) {
88
- if (error instanceof SimulationRevertedError)
89
- throw error;
90
- if (error instanceof ExternalServiceError)
91
- throw error;
92
- // A node-level "execution reverted" is a property of the bundle, not the backend.
93
- if (error instanceof ExecutionRevertedError)
94
- throw new SimulationRevertedError(error.shortMessage, error);
95
- throw new ExternalServiceError(`eth_simulateV1 error: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
52
+ throw toBoundaryError(label, error);
96
53
  }
54
+ };
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
+ });
97
73
  }
98
74
  /**
99
- * Reduce parsed transfer logs to net per-token balance changes grouped by
100
- * account. With `traceTransfers` enabled, native ETH (top-level and internal)
101
- * arrives as transfer logs under viem's `ethAddress`, so no separate top-level
102
- * `value` accounting is needed — doing so would double-count.
75
+ * 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.
81
+ *
82
+ * The boundary performs these steps:
83
+ *
84
+ * 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
87
+ * `validation: false`. Validation-off means gas is not charged, which is
88
+ * how gas is separated from economic effects. **No balance override is
89
+ * applied** — `value` transfers are funded by the sender's real native
90
+ * balance.
91
+ * 2. **Response validation** — the response is parsed before the reorg
92
+ * re-fetch so revert/mismatch evidence already in hand surfaces instead
93
+ * of being downgraded to a bypassable {@link ExternalServiceError} by a
94
+ * failing re-fetch. The simulated block must be exactly
95
+ * `stateBlockNumber` or `stateBlockNumber + 1`: geth-style nodes report
96
+ * the former's successor while Anvil reports the pinned block itself.
97
+ * The result records whatever the node returns; consumers must read
98
+ * {@link ExecutionBlock.blockNumber} and never assume +1.
99
+ * 3. **Reorg check** — the pinned state block is re-fetched last to detect a reorg
100
+ * that swapped its hash mid-flight (`InvalidSimulationResponseError`).
101
+ *
102
+ * The endpoint must support `eth_simulateV1` with per-call `from`; there is
103
+ * no fallback backend.
104
+ *
105
+ * @param params - Shared simulation client, execution plan, and already-pinned
106
+ * state block.
107
+ * @returns Deep-frozen {@link SimulationExecution} — per-transaction call
108
+ * results and the resolved {@link ExecutionBlock}.
109
+ * @throws {ExternalServiceError} For transport failures, timeouts,
110
+ * or malformed JSON-RPC envelopes.
111
+ * @throws {InvalidSimulationResponseError} For a response that cannot be
112
+ * trusted (bad shape, call-count mismatch, block
113
+ * other than the pinned state block or its successor, a block timestamp
114
+ * earlier than the pinned block's, a per-call result that fails normalization,
115
+ * or a state-block hash that changed or is no longer served mid-flight).
116
+ * @throws {MissingVerificationEvidenceError} When a planned state read fails.
117
+ * @throws {SimulationRevertedError} When a preparation or user transaction
118
+ * reverts or the node reports a bundle-level revert (code 3 / insufficient
119
+ * funds).
120
+ * @internal
103
121
  */
104
- function toAssetChanges(transfers) {
105
- const entries = [];
106
- for (const { token, from, to, amount } of transfers) {
107
- entries.push({ account: to, token, diff: amount });
108
- entries.push({ account: from, token, diff: -amount });
122
+ export async function executePlan(params) {
123
+ const { client, plan, stateBlock } = params;
124
+ const response = await rpc("eth_simulateV1", () => client.request({
125
+ method: "eth_simulateV1",
126
+ params: [
127
+ {
128
+ blockStateCalls: [
129
+ {
130
+ calls: plan.calls.map((call) => ({
131
+ from: call.transaction.from,
132
+ to: call.transaction.to,
133
+ data: call.transaction.data,
134
+ value: numberToHex(call.transaction.value),
135
+ })),
136
+ },
137
+ ],
138
+ traceTransfers: true,
139
+ validation: false,
140
+ },
141
+ numberToHex(stateBlock.number),
142
+ ],
143
+ }));
144
+ // Response parsing is validation, not transport — it runs before the
145
+ // reorg re-fetch so evidence already in hand reaches the caller as
146
+ // InvalidSimulationResponseError / SimulationRevertedError instead of
147
+ // being downgraded to a bypassable ExternalServiceError by a failing
148
+ // re-fetch.
149
+ const execution = parseSimulationResponse({
150
+ plan,
151
+ blocks: response,
152
+ stateBlockNumber: stateBlock.number,
153
+ stateBlockHash: stateBlock.hash,
154
+ stateBlockTimestamp: stateBlock.timestamp,
155
+ });
156
+ // Reorg window: the pinned state block must still carry the same hash
157
+ // after simulation, or the result may describe a different chain tip.
158
+ const stateBlockAfter = await rpc("eth_getBlock", () => client.getBlock({ blockNumber: stateBlock.number })).catch((error) => {
159
+ if (error instanceof ExternalServiceError &&
160
+ error.cause instanceof BlockNotFoundError)
161
+ return error;
162
+ throw error;
163
+ });
164
+ if (stateBlockAfter instanceof ExternalServiceError ||
165
+ stateBlockAfter.hash !== stateBlock.hash) {
166
+ const after = stateBlockAfter instanceof ExternalServiceError
167
+ ? "unavailable"
168
+ : stateBlockAfter.hash;
169
+ throw new InvalidSimulationResponseError(`State block ${stateBlock.number} hash changed during simulation (reorg): ${stateBlock.hash} became ${after}. Re-submit the simulation.`, {
170
+ context: {
171
+ stage: "transport",
172
+ chainId: plan.request.chainId,
173
+ mode: plan.request.mode,
174
+ blockNumber: stateBlock.number,
175
+ },
176
+ ...(stateBlockAfter instanceof ExternalServiceError && {
177
+ cause: stateBlockAfter.cause,
178
+ }),
179
+ });
109
180
  }
110
- return groupAssetChanges(entries);
181
+ return execution;
111
182
  }
@@ -1,2 +1 @@
1
- export { simulateV1 } from "./eth-simulate-v1.js";
2
- export { simulateTenderlyRpc } from "./tenderly-rpc.js";
1
+ export { assertEndpointChain, executePlan } from "./eth-simulate-v1.js";
@@ -1,2 +1 @@
1
- export { simulateV1 } from "./eth-simulate-v1.js";
2
- export { simulateTenderlyRpc } from "./tenderly-rpc.js";
1
+ export { assertEndpointChain, executePlan } from "./eth-simulate-v1.js";
@@ -0,0 +1,69 @@
1
+ import { type Hex } from "viem";
2
+ import type { SimulationCall } from "../../types.js";
3
+ import type { ExecutionPlan, PlannedCall } from "../plan/plan-execution.js";
4
+ import type { StateRead } from "../state/contract.js";
5
+ /** The block coordinates a simulation was pinned to and executed under.
6
+ * @internal
7
+ */
8
+ export interface ExecutionBlock {
9
+ readonly chainId: number;
10
+ readonly stateBlockNumber: bigint;
11
+ readonly stateBlockHash: Hex;
12
+ readonly stateBlockTimestamp: bigint;
13
+ readonly blockNumber: bigint;
14
+ readonly blockTimestamp: bigint;
15
+ }
16
+ /** A planned call and its normalized result. Only successful calls are carried.
17
+ * @internal
18
+ */
19
+ interface ExecutedCall {
20
+ readonly planned: PlannedCall;
21
+ readonly result: SimulationCall;
22
+ }
23
+ /** One executed state read with its raw return data. @internal */
24
+ interface ExecutedStateRead {
25
+ readonly phase: "before" | "after";
26
+ readonly read: StateRead;
27
+ readonly returnData: Hex;
28
+ }
29
+ /** The output of {@link parseSimulationResponse}: the plan, its block, successful calls and state reads.
30
+ * @internal
31
+ */
32
+ export interface SimulationExecution {
33
+ readonly plan: ExecutionPlan;
34
+ readonly block: ExecutionBlock;
35
+ readonly calls: readonly ExecutedCall[];
36
+ /** Successful state reads, in plan order, tagged by phase. */
37
+ readonly stateReads: readonly ExecutedStateRead[];
38
+ }
39
+ /**
40
+ * Parse a raw `eth_simulateV1` result into a
41
+ * {@link SimulationExecution}.
42
+ *
43
+ * The response must be exactly one block at the pinned state block or its
44
+ * immediate successor. Block advancement is node-specific: geth-style nodes
45
+ * simulate on top of `base + 1` while Anvil reports the base block itself —
46
+ * this parser records whatever the node reports in {@link ExecutionBlock}
47
+ * and rejects any other height. Consumers must read
48
+ * `block.blockNumber`/`block.blockTimestamp` and never assume +1.
49
+ *
50
+ * @param params - The plan, the raw `eth_simulateV1` result, and the
51
+ * resolved state block.
52
+ * @returns Deep-frozen execution: tagged calls plus one
53
+ * {@link ExecutedStateRead} per planned state read.
54
+ * @throws {InvalidSimulationResponseError} When the result is not exactly one
55
+ * block, on a call-count mismatch, a malformed per-call result, or on a
56
+ * simulated block that is neither the pinned state block nor its immediate successor.
57
+ * @throws {SimulationRevertedError} When a preparation or user-transaction
58
+ * call failed; the `details` payload carries tagged user call results only.
59
+ * @throws {MissingVerificationEvidenceError} When a planned state read fails.
60
+ * @internal
61
+ */
62
+ export declare function parseSimulationResponse(params: {
63
+ readonly plan: ExecutionPlan;
64
+ readonly blocks: unknown;
65
+ readonly stateBlockNumber: bigint;
66
+ readonly stateBlockHash: Hex;
67
+ readonly stateBlockTimestamp: bigint;
68
+ }): SimulationExecution;
69
+ export {};
@@ -0,0 +1,165 @@
1
+ import { deepFreeze } from "@morpho-org/morpho-ts";
2
+ import { isAddress, isHex } from "viem";
3
+ import { InvalidSimulationResponseError, MissingVerificationEvidenceError, SimulationRevertedError, } from "../../errors.js";
4
+ const isQuantity = (value) => typeof value === "string" && /^0x[0-9a-fA-F]+$/.test(value);
5
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
6
+ const readField = (object, key) => Reflect.get(object, key);
7
+ const isSimulateV1Response = (value) => Array.isArray(value) &&
8
+ value.length === 1 &&
9
+ isObject(value[0]) &&
10
+ isQuantity(readField(value[0], "number")) &&
11
+ isQuantity(readField(value[0], "timestamp")) &&
12
+ isHex(readField(value[0], "hash")) &&
13
+ Array.isArray(readField(value[0], "calls"));
14
+ /**
15
+ * Parse a raw `eth_simulateV1` result into a
16
+ * {@link SimulationExecution}.
17
+ *
18
+ * The response must be exactly one block at the pinned state block or its
19
+ * immediate successor. Block advancement is node-specific: geth-style nodes
20
+ * simulate on top of `base + 1` while Anvil reports the base block itself —
21
+ * this parser records whatever the node reports in {@link ExecutionBlock}
22
+ * and rejects any other height. Consumers must read
23
+ * `block.blockNumber`/`block.blockTimestamp` and never assume +1.
24
+ *
25
+ * @param params - The plan, the raw `eth_simulateV1` result, and the
26
+ * resolved state block.
27
+ * @returns Deep-frozen execution: tagged calls plus one
28
+ * {@link ExecutedStateRead} per planned state read.
29
+ * @throws {InvalidSimulationResponseError} When the result is not exactly one
30
+ * block, on a call-count mismatch, a malformed per-call result, or on a
31
+ * simulated block that is neither the pinned state block nor its immediate successor.
32
+ * @throws {SimulationRevertedError} When a preparation or user-transaction
33
+ * call failed; the `details` payload carries tagged user call results only.
34
+ * @throws {MissingVerificationEvidenceError} When a planned state read fails.
35
+ * @internal
36
+ */
37
+ export function parseSimulationResponse(params) {
38
+ const { plan, blocks } = params;
39
+ const errorContext = {
40
+ stage: "transport",
41
+ chainId: plan.request.chainId,
42
+ mode: plan.request.mode,
43
+ blockNumber: params.stateBlockNumber,
44
+ };
45
+ if (!isSimulateV1Response(blocks)) {
46
+ throw new InvalidSimulationResponseError("eth_simulateV1 returned an unexpected response shape. Check that the configured endpoint implements eth_simulateV1.", { context: errorContext });
47
+ }
48
+ const block = blocks[0];
49
+ const blockNumber = BigInt(block.number);
50
+ const blockTimestamp = BigInt(block.timestamp);
51
+ if (blockNumber !== params.stateBlockNumber &&
52
+ blockNumber !== params.stateBlockNumber + 1n) {
53
+ throw new InvalidSimulationResponseError(`eth_simulateV1 reported block ${blockNumber} but the pinned state block is ${params.stateBlockNumber}; the node did not honor the pinned block.`, { context: errorContext });
54
+ }
55
+ // Anvil re-hashes the pinned block, so only the geth-style successor can be
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 &&
59
+ block.parentHash !== params.stateBlockHash) {
60
+ 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
+ }
62
+ if (blockTimestamp < params.stateBlockTimestamp) {
63
+ throw new InvalidSimulationResponseError(`eth_simulateV1 reported block timestamp ${blockTimestamp}, behind the pinned state block timestamp ${params.stateBlockTimestamp}. Check that the endpoint executes on top of the requested block.`, { context: errorContext });
64
+ }
65
+ if (block.calls.length !== plan.calls.length) {
66
+ throw new InvalidSimulationResponseError(`eth_simulateV1 returned ${block.calls.length} call result(s) for ${plan.calls.length} planned call(s). Refusing to map the response with mismatched lengths.`, { context: errorContext });
67
+ }
68
+ const calls = block.calls.map((call, index) => {
69
+ const planned = plan.calls[index];
70
+ let result;
71
+ try {
72
+ if (!isObject(call) ||
73
+ (call.status !== "0x0" && call.status !== "0x1") ||
74
+ !isHex(call.returnData) ||
75
+ !isQuantity(call.gasUsed) ||
76
+ (call.logs != null && !Array.isArray(call.logs)))
77
+ throw new InvalidSimulationResponseError(`eth_simulateV1 returned a malformed call result at position ${index}.`, { context: errorContext });
78
+ result = {
79
+ logs: (call.logs ?? []).map((log) => {
80
+ if (!isAddress(log.address) ||
81
+ !Array.isArray(log.topics) ||
82
+ log.topics.some((topic) => !isHex(topic)) ||
83
+ (log.data !== undefined && !isHex(log.data)))
84
+ throw new InvalidSimulationResponseError(`eth_simulateV1 returned a malformed log at call position ${index}.`, { context: errorContext });
85
+ return {
86
+ address: log.address,
87
+ topics: log.topics,
88
+ data: log.data ?? "0x",
89
+ };
90
+ }),
91
+ status: call.status === "0x1",
92
+ returnData: call.returnData,
93
+ gasUsed: BigInt(call.gasUsed),
94
+ };
95
+ }
96
+ catch (cause) {
97
+ if (cause instanceof InvalidSimulationResponseError)
98
+ throw cause;
99
+ throw new InvalidSimulationResponseError(`eth_simulateV1 returned a malformed call result at position ${index}.`, { context: errorContext, cause });
100
+ }
101
+ return { planned, result, call };
102
+ });
103
+ // A preparation call that reverted did not produce its promised state —
104
+ // it is reported before user-transaction reverts it caused.
105
+ const failedPreparation = calls.find((entry) => entry.planned.type === "preparation" && !entry.result.status);
106
+ if (failedPreparation) {
107
+ const message = failedPreparation.call.error?.message;
108
+ throw new SimulationRevertedError(`Authorization preparation call failed during simulation${message !== undefined ? `: ${message}` : ""}. Re-submit the bundle; if it persists, check that the endpoint executes preparation calls.`, undefined, "UNKNOWN_REVERT", {
109
+ stage: "preparation",
110
+ chainId: plan.request.chainId,
111
+ mode: plan.request.mode,
112
+ blockNumber: params.stateBlockNumber,
113
+ authorizationIndex: failedPreparation.planned.authorizationIndex,
114
+ preparationCallIndex: failedPreparation.planned.callIndex,
115
+ });
116
+ }
117
+ // A user-transaction revert belongs to the bundle, not the boundary.
118
+ const failedUserCall = calls.find(({ planned, result }) => planned.type === "transaction" && !result.status);
119
+ if (failedUserCall && failedUserCall.planned.type === "transaction") {
120
+ throw new SimulationRevertedError(failedUserCall.call.error?.message ?? "Simulation failed", deepFreeze(calls
121
+ .filter((entry) => entry.planned.type === "transaction")
122
+ .map(({ planned, result }) => ({
123
+ transactionIndex: planned.transactionIndex,
124
+ result,
125
+ }))), "UNKNOWN_REVERT");
126
+ }
127
+ const stateReads = [];
128
+ for (const { planned, result, call } of calls) {
129
+ if (planned.type !== "stateRead")
130
+ continue;
131
+ if (!result.status) {
132
+ const message = call.error?.message;
133
+ throw new MissingVerificationEvidenceError(`State read "${planned.read.id}" failed during simulation${message !== undefined ? `: ${message}` : ""}. Re-submit the bundle; if it persists, check that the endpoint executes view calls in the same block.`, {
134
+ context: {
135
+ stage: "verification",
136
+ chainId: plan.request.chainId,
137
+ mode: plan.request.mode,
138
+ blockNumber: params.stateBlockNumber,
139
+ field: planned.read.id,
140
+ },
141
+ });
142
+ }
143
+ stateReads.push({
144
+ phase: planned.phase,
145
+ read: planned.read,
146
+ returnData: call.returnData,
147
+ });
148
+ }
149
+ return deepFreeze({
150
+ plan,
151
+ block: {
152
+ chainId: plan.request.chainId,
153
+ stateBlockNumber: params.stateBlockNumber,
154
+ stateBlockHash: params.stateBlockHash,
155
+ stateBlockTimestamp: params.stateBlockTimestamp,
156
+ blockNumber,
157
+ blockTimestamp,
158
+ },
159
+ // Every failure was classified above: only successful calls remain.
160
+ calls: calls
161
+ .filter((entry) => entry.result.status)
162
+ .map(({ planned, result }) => ({ planned, result })),
163
+ stateReads,
164
+ });
165
+ }
@@ -0,0 +1,26 @@
1
+ import { type Address, type Client } from "viem";
2
+ import type { OperationLimit } from "../../limits.js";
3
+ import type { SimulationMode } from "../../params.js";
4
+ /** Caller-selected operation with only the quoted wallet assets resolved. @internal */
5
+ export interface ResolvedSlippageOperation {
6
+ readonly limit: OperationLimit;
7
+ readonly assetsPaid?: Address;
8
+ readonly assetsReceived?: Address;
9
+ }
10
+ /**
11
+ * Resolve underlying tokens only when an asset quote lacks an explicit token.
12
+ * Share-only quotes need no RPC reads; vault versions and factories are not discovered.
13
+ * @param params - Caller operations, Morpho address, client, request context, and pinned state block.
14
+ * @returns Operations with the token addresses required by their asset quotes.
15
+ * @throws {ExternalServiceError} For transport failures during a required metadata read.
16
+ * @throws {MissingVerificationEvidenceError} For reverted, malformed, or empty metadata.
17
+ * @internal
18
+ */
19
+ export declare function resolveAssets(params: {
20
+ readonly client: Client;
21
+ readonly morpho: Address;
22
+ readonly operations: readonly OperationLimit[];
23
+ readonly chainId: number;
24
+ readonly mode: SimulationMode;
25
+ readonly blockNumber: bigint;
26
+ }): Promise<readonly ResolvedSlippageOperation[]>;