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