@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
@@ -0,0 +1,19 @@
1
+ import type { AuthorizationPreparation, SimulatedOperation, VerifiedSimulationResult } from "../result.js";
2
+ import type { AccountAssetChanges, SimulationCall, Transfer } from "../types.js";
3
+ import type { CheckContext } from "./context.js";
4
+ import type { ParsedRequest } from "./request/parse-request.js";
5
+ /**
6
+ * Assemble the public result from the checked pipeline. Only user
7
+ * transactions appear in `simulationTxs`/`calls`/`transfers` — preparation
8
+ * calls and state reads never get a public `txIdx`.
9
+ * @internal
10
+ */
11
+ export declare function assembleResult(params: {
12
+ readonly ctx: CheckContext;
13
+ readonly request: ParsedRequest;
14
+ readonly operations: readonly SimulatedOperation[];
15
+ readonly authorizations: readonly AuthorizationPreparation[];
16
+ readonly userCalls: readonly SimulationCall[];
17
+ readonly transfers: readonly Transfer[];
18
+ readonly assetChanges: readonly AccountAssetChanges[];
19
+ }): VerifiedSimulationResult;
@@ -0,0 +1,26 @@
1
+ import { deepFreeze } from "@morpho-org/morpho-ts";
2
+ /**
3
+ * Assemble the public result from the checked pipeline. Only user
4
+ * transactions appear in `simulationTxs`/`calls`/`transfers` — preparation
5
+ * calls and state reads never get a public `txIdx`.
6
+ * @internal
7
+ */
8
+ export function assembleResult(params) {
9
+ const { ctx, request, operations, authorizations, userCalls, transfers, assetChanges, } = params;
10
+ const verification = {
11
+ mode: ctx.mode,
12
+ chainId: ctx.chainId,
13
+ blockNumber: ctx.block.blockNumber,
14
+ blockTimestamp: ctx.block.blockTimestamp,
15
+ limits: ctx.limits,
16
+ operations,
17
+ authorizations,
18
+ };
19
+ return deepFreeze({
20
+ simulationTxs: request.transactions,
21
+ calls: userCalls,
22
+ transfers,
23
+ assetChanges,
24
+ verification,
25
+ });
26
+ }
@@ -0,0 +1,18 @@
1
+ import type { VerifiedSimulationResult } from "../result.js";
2
+ import type { SimulationConfig } from "../types.js";
3
+ import type { ParsedRequest } from "./request/parse-request.js";
4
+ /**
5
+ * Run the verified simulation pipeline in a single `eth_simulateV1` call:
6
+ * resolve quoted assets → plan quoted observations → execution →
7
+ * slippage comparisons → bundle-retention assertion → result assembly.
8
+ *
9
+ * One `AbortSignal` (the request timeout) covers every RPC step.
10
+ *
11
+ * @internal
12
+ * @param params - The simulation config and the parsed caller request.
13
+ * @returns The deep-frozen {@link VerifiedSimulationResult}.
14
+ */
15
+ export declare function runSimulation(params: {
16
+ readonly config: SimulationConfig;
17
+ readonly request: ParsedRequest;
18
+ }): Promise<VerifiedSimulationResult>;
@@ -0,0 +1,148 @@
1
+ import { getChainAddresses, UnsupportedChainIdError, } from "@morpho-org/blue-sdk";
2
+ import { _try } from "@morpho-org/morpho-ts";
3
+ import { zeroAddress } from "viem";
4
+ import { InvalidSimulationResponseError, UnsupportedChainError, } from "../errors.js";
5
+ import { groupAssetChanges } from "./asset-changes.js";
6
+ import { createSimulationClient } from "./backends/client.js";
7
+ import { assertEndpointChain, executePlan } from "./backends/index.js";
8
+ import { resolveAssets } from "./backends/resolve-assets.js";
9
+ import { resolvePinnedBlock } from "./backends/resolve-pinned-block.js";
10
+ import { parseTransfers } from "./parsing/index.js";
11
+ import { assertNoBundlesRetention } from "./pipeline/bundles-retention.js";
12
+ import { resolveChain } from "./pipeline/resolve-chain.js";
13
+ import { planExecution } from "./plan/plan-execution.js";
14
+ import { prepareAuthorizations } from "./prepare.js";
15
+ import { assembleResult } from "./result.js";
16
+ import { decodeStateRead, planStateReads } from "./state/read-state.js";
17
+ import { verifySlippage } from "./verify-slippage.js";
18
+ /** Total execution budget for a single `simulate()` call. */
19
+ const DEFAULT_TIMEOUT_MS = 5000;
20
+ /**
21
+ * Run the verified simulation pipeline in a single `eth_simulateV1` call:
22
+ * resolve quoted assets → plan quoted observations → execution →
23
+ * slippage comparisons → bundle-retention assertion → result assembly.
24
+ *
25
+ * One `AbortSignal` (the request timeout) covers every RPC step.
26
+ *
27
+ * @internal
28
+ * @param params - The simulation config and the parsed caller request.
29
+ * @returns The deep-frozen {@link VerifiedSimulationResult}.
30
+ */
31
+ export async function runSimulation(params) {
32
+ const { config, request } = params;
33
+ const chain = resolveChain(config, request.chainId);
34
+ const signal = AbortSignal.timeout(config.timeoutMs ?? DEFAULT_TIMEOUT_MS);
35
+ const client = createSimulationClient(chain.simulateV1Url, signal);
36
+ const limits = { operations: request.limits?.operations ?? [] };
37
+ const preview = request.mode === "preview";
38
+ const owner = request.transactions[0].from;
39
+ const addresses = _try(() => getChainAddresses(request.chainId), UnsupportedChainIdError);
40
+ const requiresMorpho = limits.operations.length > 0 ||
41
+ (preview && request.authorizations.length > 0);
42
+ if (requiresMorpho && addresses?.blue == null)
43
+ throw new UnsupportedChainError(request.chainId);
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
+ const pinnedBlock = await resolvePinnedBlock({
52
+ client,
53
+ blockNumber: request.blockNumber,
54
+ signal,
55
+ });
56
+ // With no calldata decoding, the caller's declared `limits.operations`
57
+ // entries are the operations: subjects for state reads and per-operation
58
+ // checks come straight from the limit list.
59
+ const resolved = await resolveAssets({
60
+ client,
61
+ morpho,
62
+ operations: limits.operations,
63
+ chainId: request.chainId,
64
+ mode: request.mode,
65
+ blockNumber: pinnedBlock.number,
66
+ });
67
+ const observations = planStateReads({ operations: resolved, owner, morpho });
68
+ const preparations = preview
69
+ ? prepareAuthorizations({
70
+ authorizations: request.authorizations,
71
+ owner,
72
+ morpho,
73
+ })
74
+ : [];
75
+ const plan = planExecution({
76
+ request,
77
+ owner,
78
+ preparations,
79
+ reads: observations.reads,
80
+ });
81
+ // Execute against the block resolved once above; the boundary rechecks its hash.
82
+ const execution = await executePlan({
83
+ client,
84
+ plan,
85
+ stateBlock: pinnedBlock,
86
+ });
87
+ const userCalls = execution.calls
88
+ .filter((call) => call.planned.type === "transaction")
89
+ .map((call) => call.result);
90
+ if (userCalls.length !== request.transactions.length)
91
+ throw new InvalidSimulationResponseError(`Execution returned ${userCalls.length} user call result(s) for ${request.transactions.length} transaction(s)`);
92
+ const decodePhase = (phase) => new Map(execution.stateReads
93
+ .filter((read) => read.phase === phase)
94
+ .map((read) => [
95
+ read.read.id,
96
+ decodeStateRead(read.read, read.returnData, {
97
+ chainId: request.chainId,
98
+ mode: request.mode,
99
+ blockNumber: pinnedBlock.number,
100
+ }),
101
+ ]));
102
+ const transfers = parseTransfers(userCalls, {
103
+ wNative: addresses === undefined ? undefined : (addresses.wNative ?? null),
104
+ logger: config.logger,
105
+ });
106
+ const assetChanges = groupAssetChanges(transfers.flatMap(({ token, from, to, amount }) => [
107
+ { account: to, token, diff: amount },
108
+ { account: from, token, diff: -amount },
109
+ ]));
110
+ const ctx = {
111
+ chainId: request.chainId,
112
+ mode: request.mode,
113
+ block: execution.block,
114
+ owner,
115
+ limits,
116
+ };
117
+ const authorizations = request.authorizations.map((authorization, authorizationIndex) => ({
118
+ authorizationIndex,
119
+ authorization,
120
+ calls: execution.calls.flatMap(({ planned, result }) => planned.type === "preparation" &&
121
+ planned.authorizationIndex === authorizationIndex
122
+ ? [{ transaction: planned.transaction, result }]
123
+ : []),
124
+ }));
125
+ const checked = verifySlippage({
126
+ ctx,
127
+ operations: observations.operations,
128
+ before: decodePhase("before"),
129
+ after: decodePhase("after"),
130
+ transfers,
131
+ requestTransactions: request.transactions,
132
+ });
133
+ assertNoBundlesRetention({
134
+ chainId: request.chainId,
135
+ transfers,
136
+ assetChanges,
137
+ logger: config.logger,
138
+ });
139
+ return assembleResult({
140
+ ctx,
141
+ request,
142
+ operations: checked,
143
+ authorizations,
144
+ userCalls,
145
+ transfers,
146
+ assetChanges,
147
+ });
148
+ }
@@ -1,67 +1,117 @@
1
- import type { SimulateParams, SimulationConfig, SimulationResult } from "../types.js";
1
+ import type { SimulateParams } from "../params.js";
2
+ import type { VerifiedSimulationResult } from "../result.js";
3
+ import type { SimulationConfig } from "../types.js";
2
4
  /**
3
5
  * Simulate a bundle of EVM transactions.
4
6
  *
5
- * Validates input → resolves authorizations into prepended approve txs → runs the bundle
6
- * through Tenderly RPC (primary) or `eth_simulateV1` (fallback) with a shared timeout
7
- * budget → parses ERC20 transfers and WETH9 events from per-tx logs, restricting WETH9
8
- * events to the registered wrapped-native token, rejecting them on known tokenless chains,
9
- * and retaining signature-based parsing for unknown chains → asserts no funds are retained
10
- * by standalone `bundles` periphery contracts → returns the full result
11
- * set. The caller reads whichever fields they need:
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
12
+ * user calls only → decodes quoted balances/positions → runs slippage checks
13
+ * → asserts no funds are retained by standalone `bundles` periphery contracts
14
+ * → returns the result. The caller reads whichever fields they need:
12
15
  *
13
- * - `transfers` → user-facing preview / server-side verification.
14
- * - `simulationTxs` + `transfers` → server-side verification before broadcast.
15
- * - `calls[i]` → per-tx raw backend output (`logs`, `status`, `returnData`, `gasUsed`).
16
- * Aligned 1:1 with `simulationTxs[i]`. `gasUsed` is not a safe gas limit; consumers
17
- * deriving one must add their own headroom.
18
- * - `assetChanges` → net per-asset balance changes grouped by account (sender and
19
- * counterparties) over the whole bundle, normalized to the same shape across backends.
20
- * - `transfers[k].txIdx` → index into `simulationTxs` of the tx that emitted the
21
- * underlying log; consumers map back via `simulationTxs[transfer.txIdx]`.
16
+ * - `simulationTxs` → exactly the caller's ordered transactions, normalized
17
+ * (checksummed addresses, `value` defaulted to `0n`). Internal state reads are
18
+ * never exposed.
19
+ * - `calls[i]` → per-tx raw backend output (`logs`, `status`, `returnData`,
20
+ * `gasUsed`), aligned 1:1 with `simulationTxs[i]`. `gasUsed` is not a safe
21
+ * gas limit; consumers deriving one must add their own headroom.
22
+ * - `transfers` → user-facing preview / server-side verification; each
23
+ * transfer's `txIdx` indexes into `simulationTxs`.
24
+ * - `assetChanges` → net per-asset balance changes grouped by account (sender
25
+ * and counterparties) over the whole bundle.
22
26
  *
23
- * @param config - Backend configuration: per-chain Tenderly RPC and/or `eth_simulateV1`
24
- * URL, optional logger, and the overall timeout budget.
27
+ * **Modes.** `mode` defaults to `"final"`, which executes the signed calldata
28
+ * against actual permissions and accepts no `authorizations`. `mode:
29
+ * "preview"` accepts typed authorization descriptors, which the pipeline
30
+ * prepares as simulated approval calls. No authorization read-back checks run.
31
+ * `limits` are optional caller-supplied quotes and percentage tolerances;
32
+ * violations throw `ConsumerLimitViolationError`.
33
+ *
34
+ * **Funding.** `value` transfers are funded by the sender's real native
35
+ * balance — no balance inflation — so under-funded bundles revert exactly as
36
+ * they would on-chain. `validation: false` keeps gas from being charged,
37
+ * separating gas from economic effects.
38
+ *
39
+ * @param config - Required per-chain `eth_simulateV1` URL, optional logger, and
40
+ * the overall timeout budget.
25
41
  * @param params - Per-call simulation input.
26
- * @param params.chainId - Chain id the bundle targets.
27
- * @param params.transactions - The bundle's transactions, in execution order. All must share the
28
- * same `from`.
29
- * @param params.authorizations - Optional token authorizations resolved into prepended approve
30
- * transactions before the main bundle runs.
31
- * @param params.blockNumber - Optional pinned block number or `BlockTag`. Defaults to `latest`.
32
- * @throws {SimulationValidationError} for invalid input (mixed senders, bad addresses,
33
- * empty transactions, malformed authorizations).
34
- * @throws {UnsupportedChainError} when the chain is not configured for any backend.
35
- * @throws {SimulationRevertedError} when the bundle reverts on either backend.
36
- * @throws {BlacklistViolationError} when the simulation leaves value retained beyond
37
- * the dust threshold by a `bundles` periphery contract (VaultExitBundlesV1,
38
- * VaultBundlesV1, BlueBundlesV1). Never bypassable.
39
- * @throws {ExternalServiceError} (a) when both backends are unavailable within the
40
- * timeout budget, or (b) when a backend returns a `calls` array whose length does
41
- * not match the resolved `simulationTxs` — refusing to map transfers with mismatched
42
- * per-tx output.
43
- * @returns A {@link SimulationResult} carrying the resolved `simulationTxs`, per-tx
44
- * `calls` (aligned 1:1 with `simulationTxs`), parsed `transfers` (each stamped
45
- * with `txIdx`), and per-account net `assetChanges`.
42
+ * @param params.chainId - Chain id the bundle targets; must match the endpoint.
43
+ * @param params.transactions - The bundle's transactions, in execution order.
44
+ * All must share the same `from`.
45
+ * @param params.mode - `"final"` (default) or `"preview"`.
46
+ * @param params.authorizations - Preview-only typed authorization descriptors.
47
+ * @param params.limits - Optional quotes and percentage tolerances; unquoted amounts require no reads.
48
+ * @param params.blockNumber - Optional pinned block number or `BlockTag` other
49
+ * than `"pending"`.
50
+ * Defaults to `latest`, resolved exactly once.
51
+ * @throws {SimulationValidationError} for invalid input (mixed senders, bad
52
+ * addresses, empty transactions, malformed authorizations, final-mode
53
+ * authorizations, malformed limits, unknown fields, a `"pending"` block tag, or share quotes for
54
+ * `blueSupplyCollateral` / `blueWithdrawCollateral`).
55
+ * @throws {ConsumerLimitViolationError} when a declared `limits` bound is
56
+ * violated by the observed effects.
57
+ * @throws {UnsupportedChainError} when the chain has no `eth_simulateV1`
58
+ * endpoint configured, or limits/preview authorizations require a Morpho
59
+ * Blue address absent from blue-sdk's `getChainAddresses`.
60
+ * @throws {SimulationRevertedError} when a preparation or user transaction reverts.
61
+ * @throws {MissingVerificationEvidenceError} when a planned state read fails
62
+ * or returns empty data, native outgoing traces do not cover value
63
+ * sent, or required metadata reverts or returns empty/invalid data.
64
+ * @throws {InvalidSimulationResponseError} when the node response cannot be
65
+ * trusted (bad shape, call-count mismatch, block that is neither the pinned
66
+ * state block nor its immediate successor, a successor with a mismatched
67
+ * `parentHash`, a block timestamp earlier than the pinned block's, a
68
+ * malformed per-call result, a quoted balance/position read whose non-empty
69
+ * return data cannot be decoded, a state-block hash that changed or a pinned
70
+ * block that vanished mid-flight, or an
71
+ * endpoint whose `eth_chainId` differs from `params.chainId`; chain identity
72
+ * is checked before any block lookup).
73
+ * @throws {BlacklistViolationError} when the simulation leaves value retained
74
+ * beyond the dust threshold by a `bundles` periphery contract
75
+ * (VaultExitBundlesV1, VaultBundlesV1, BlueBundlesV1, MidnightBundlesV1).
76
+ * Never bypassable.
77
+ * @throws {ExternalServiceError} when the RPC is unavailable within the
78
+ * timeout budget or returns a malformed JSON-RPC envelope. Chain-id
79
+ * mismatches and a state block without number/hash are reported as
80
+ * `InvalidSimulationResponseError`.
81
+ * @returns A frozen {@link VerifiedSimulationResult} carrying the normalized
82
+ * `simulationTxs`, per-tx `calls` (aligned 1:1), parsed `transfers` (each
83
+ * stamped with `txIdx`), per-account net `assetChanges`, and per-operation
84
+ * verification outcomes and preparations. Each `verification.operations[i]`
85
+ * mirrors the `limits.operations[i]` entry it verified. Only the amounts
86
+ * quoted in each entry are checked; `checkedLimits` identifies precisely
87
+ * what was checked. Measurements are scoped to the whole bundle.
46
88
  * @example
47
89
  * ```ts
48
90
  * import { simulate } from "@morpho-org/evm-simulation";
91
+ * import { type Address, encodeFunctionData, erc20Abi, getAddress } from "viem";
92
+ *
93
+ * const rpcUrl = "https://rpc.example";
94
+ * const user: Address = getAddress("0x1111111111111111111111111111111111111111");
95
+ * const usdc: Address = getAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
96
+ * const recipient: Address = getAddress("0x2222222222222222222222222222222222222222");
49
97
  *
50
98
  * const result = await simulate(
51
- * {
52
- * chains: new Map([
53
- * [1, {
54
- * tenderlyRpc: { rpcUrl: process.env.TENDERLY_RPC_URL! },
55
- * simulateV1Url: process.env.MAINNET_RPC_URL,
56
- * }],
57
- * ]),
58
- * },
99
+ * { chains: new Map([[1, { simulateV1Url: rpcUrl }]]) },
59
100
  * {
60
101
  * chainId: 1,
61
- * transactions: [{ from: user, to: vaultAddress, data: encodedCalldata, value: 0n }],
102
+ * transactions: [
103
+ * {
104
+ * from: user,
105
+ * to: usdc,
106
+ * data: encodeFunctionData({
107
+ * abi: erc20Abi,
108
+ * functionName: "transfer",
109
+ * args: [recipient, 1_000_000n],
110
+ * }),
111
+ * },
112
+ * ],
62
113
  * },
63
114
  * );
64
- * // result satisfies SimulationResult
65
115
  * ```
66
116
  */
67
- export declare function simulate(config: SimulationConfig, params: SimulateParams): Promise<SimulationResult>;
117
+ export declare function simulate(config: SimulationConfig, params: SimulateParams): Promise<VerifiedSimulationResult>;
@@ -1,100 +1,118 @@
1
- import { _try, getChainAddresses, UnsupportedChainIdError, } from "@morpho-org/blue-sdk";
2
- import { ExternalServiceError } from "../errors.js";
3
- import { parseTransfers } from "./parsing/index.js";
4
- import { assertNoBundlesRetention, buildSimulationTxs, executeSimulation, validateInput, } from "./pipeline/index.js";
1
+ import { parseRequest } from "./request/index.js";
2
+ import { runSimulation } from "./run-simulation.js";
5
3
  /**
6
4
  * Simulate a bundle of EVM transactions.
7
5
  *
8
- * Validates input → resolves authorizations into prepended approve txs → runs the bundle
9
- * through Tenderly RPC (primary) or `eth_simulateV1` (fallback) with a shared timeout
10
- * budget → parses ERC20 transfers and WETH9 events from per-tx logs, restricting WETH9
11
- * events to the registered wrapped-native token, rejecting them on known tokenless chains,
12
- * and retaining signature-based parsing for unknown chains → asserts no funds are retained
13
- * by standalone `bundles` periphery contracts → returns the full result
14
- * set. The caller reads whichever fields they need:
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
11
+ * user calls only → decodes quoted balances/positions → runs slippage checks
12
+ * → asserts no funds are retained by standalone `bundles` periphery contracts
13
+ * → returns the result. The caller reads whichever fields they need:
15
14
  *
16
- * - `transfers` → user-facing preview / server-side verification.
17
- * - `simulationTxs` + `transfers` → server-side verification before broadcast.
18
- * - `calls[i]` → per-tx raw backend output (`logs`, `status`, `returnData`, `gasUsed`).
19
- * Aligned 1:1 with `simulationTxs[i]`. `gasUsed` is not a safe gas limit; consumers
20
- * deriving one must add their own headroom.
21
- * - `assetChanges` → net per-asset balance changes grouped by account (sender and
22
- * counterparties) over the whole bundle, normalized to the same shape across backends.
23
- * - `transfers[k].txIdx` → index into `simulationTxs` of the tx that emitted the
24
- * underlying log; consumers map back via `simulationTxs[transfer.txIdx]`.
15
+ * - `simulationTxs` → exactly the caller's ordered transactions, normalized
16
+ * (checksummed addresses, `value` defaulted to `0n`). Internal state reads are
17
+ * never exposed.
18
+ * - `calls[i]` → per-tx raw backend output (`logs`, `status`, `returnData`,
19
+ * `gasUsed`), aligned 1:1 with `simulationTxs[i]`. `gasUsed` is not a safe
20
+ * gas limit; consumers deriving one must add their own headroom.
21
+ * - `transfers` → user-facing preview / server-side verification; each
22
+ * transfer's `txIdx` indexes into `simulationTxs`.
23
+ * - `assetChanges` → net per-asset balance changes grouped by account (sender
24
+ * and counterparties) over the whole bundle.
25
25
  *
26
- * @param config - Backend configuration: per-chain Tenderly RPC and/or `eth_simulateV1`
27
- * URL, optional logger, and the overall timeout budget.
26
+ * **Modes.** `mode` defaults to `"final"`, which executes the signed calldata
27
+ * against actual permissions and accepts no `authorizations`. `mode:
28
+ * "preview"` accepts typed authorization descriptors, which the pipeline
29
+ * prepares as simulated approval calls. No authorization read-back checks run.
30
+ * `limits` are optional caller-supplied quotes and percentage tolerances;
31
+ * violations throw `ConsumerLimitViolationError`.
32
+ *
33
+ * **Funding.** `value` transfers are funded by the sender's real native
34
+ * balance — no balance inflation — so under-funded bundles revert exactly as
35
+ * they would on-chain. `validation: false` keeps gas from being charged,
36
+ * separating gas from economic effects.
37
+ *
38
+ * @param config - Required per-chain `eth_simulateV1` URL, optional logger, and
39
+ * the overall timeout budget.
28
40
  * @param params - Per-call simulation input.
29
- * @param params.chainId - Chain id the bundle targets.
30
- * @param params.transactions - The bundle's transactions, in execution order. All must share the
31
- * same `from`.
32
- * @param params.authorizations - Optional token authorizations resolved into prepended approve
33
- * transactions before the main bundle runs.
34
- * @param params.blockNumber - Optional pinned block number or `BlockTag`. Defaults to `latest`.
35
- * @throws {SimulationValidationError} for invalid input (mixed senders, bad addresses,
36
- * empty transactions, malformed authorizations).
37
- * @throws {UnsupportedChainError} when the chain is not configured for any backend.
38
- * @throws {SimulationRevertedError} when the bundle reverts on either backend.
39
- * @throws {BlacklistViolationError} when the simulation leaves value retained beyond
40
- * the dust threshold by a `bundles` periphery contract (VaultExitBundlesV1,
41
- * VaultBundlesV1, BlueBundlesV1). Never bypassable.
42
- * @throws {ExternalServiceError} (a) when both backends are unavailable within the
43
- * timeout budget, or (b) when a backend returns a `calls` array whose length does
44
- * not match the resolved `simulationTxs` — refusing to map transfers with mismatched
45
- * per-tx output.
46
- * @returns A {@link SimulationResult} carrying the resolved `simulationTxs`, per-tx
47
- * `calls` (aligned 1:1 with `simulationTxs`), parsed `transfers` (each stamped
48
- * with `txIdx`), and per-account net `assetChanges`.
41
+ * @param params.chainId - Chain id the bundle targets; must match the endpoint.
42
+ * @param params.transactions - The bundle's transactions, in execution order.
43
+ * All must share the same `from`.
44
+ * @param params.mode - `"final"` (default) or `"preview"`.
45
+ * @param params.authorizations - Preview-only typed authorization descriptors.
46
+ * @param params.limits - Optional quotes and percentage tolerances; unquoted amounts require no reads.
47
+ * @param params.blockNumber - Optional pinned block number or `BlockTag` other
48
+ * than `"pending"`.
49
+ * Defaults to `latest`, resolved exactly once.
50
+ * @throws {SimulationValidationError} for invalid input (mixed senders, bad
51
+ * addresses, empty transactions, malformed authorizations, final-mode
52
+ * authorizations, malformed limits, unknown fields, a `"pending"` block tag, or share quotes for
53
+ * `blueSupplyCollateral` / `blueWithdrawCollateral`).
54
+ * @throws {ConsumerLimitViolationError} when a declared `limits` bound is
55
+ * violated by the observed effects.
56
+ * @throws {UnsupportedChainError} when the chain has no `eth_simulateV1`
57
+ * endpoint configured, or limits/preview authorizations require a Morpho
58
+ * Blue address absent from blue-sdk's `getChainAddresses`.
59
+ * @throws {SimulationRevertedError} when a preparation or user transaction reverts.
60
+ * @throws {MissingVerificationEvidenceError} when a planned state read fails
61
+ * or returns empty data, native outgoing traces do not cover value
62
+ * sent, or required metadata reverts or returns empty/invalid data.
63
+ * @throws {InvalidSimulationResponseError} when the node response cannot be
64
+ * trusted (bad shape, call-count mismatch, block that is neither the pinned
65
+ * state block nor its immediate successor, a successor with a mismatched
66
+ * `parentHash`, a block timestamp earlier than the pinned block's, a
67
+ * malformed per-call result, a quoted balance/position read whose non-empty
68
+ * return data cannot be decoded, a state-block hash that changed or a pinned
69
+ * block that vanished mid-flight, or an
70
+ * endpoint whose `eth_chainId` differs from `params.chainId`; chain identity
71
+ * is checked before any block lookup).
72
+ * @throws {BlacklistViolationError} when the simulation leaves value retained
73
+ * beyond the dust threshold by a `bundles` periphery contract
74
+ * (VaultExitBundlesV1, VaultBundlesV1, BlueBundlesV1, MidnightBundlesV1).
75
+ * Never bypassable.
76
+ * @throws {ExternalServiceError} when the RPC is unavailable within the
77
+ * timeout budget or returns a malformed JSON-RPC envelope. Chain-id
78
+ * mismatches and a state block without number/hash are reported as
79
+ * `InvalidSimulationResponseError`.
80
+ * @returns A frozen {@link VerifiedSimulationResult} carrying the normalized
81
+ * `simulationTxs`, per-tx `calls` (aligned 1:1), parsed `transfers` (each
82
+ * stamped with `txIdx`), per-account net `assetChanges`, and per-operation
83
+ * verification outcomes and preparations. Each `verification.operations[i]`
84
+ * mirrors the `limits.operations[i]` entry it verified. Only the amounts
85
+ * quoted in each entry are checked; `checkedLimits` identifies precisely
86
+ * what was checked. Measurements are scoped to the whole bundle.
49
87
  * @example
50
88
  * ```ts
51
89
  * import { simulate } from "@morpho-org/evm-simulation";
90
+ * import { type Address, encodeFunctionData, erc20Abi, getAddress } from "viem";
91
+ *
92
+ * const rpcUrl = "https://rpc.example";
93
+ * const user: Address = getAddress("0x1111111111111111111111111111111111111111");
94
+ * const usdc: Address = getAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
95
+ * const recipient: Address = getAddress("0x2222222222222222222222222222222222222222");
52
96
  *
53
97
  * const result = await simulate(
54
- * {
55
- * chains: new Map([
56
- * [1, {
57
- * tenderlyRpc: { rpcUrl: process.env.TENDERLY_RPC_URL! },
58
- * simulateV1Url: process.env.MAINNET_RPC_URL,
59
- * }],
60
- * ]),
61
- * },
98
+ * { chains: new Map([[1, { simulateV1Url: rpcUrl }]]) },
62
99
  * {
63
100
  * chainId: 1,
64
- * transactions: [{ from: user, to: vaultAddress, data: encodedCalldata, value: 0n }],
101
+ * transactions: [
102
+ * {
103
+ * from: user,
104
+ * to: usdc,
105
+ * data: encodeFunctionData({
106
+ * abi: erc20Abi,
107
+ * functionName: "transfer",
108
+ * args: [recipient, 1_000_000n],
109
+ * }),
110
+ * },
111
+ * ],
65
112
  * },
66
113
  * );
67
- * // result satisfies SimulationResult
68
114
  * ```
69
115
  */
70
116
  export async function simulate(config, params) {
71
- validateInput(params);
72
- const wNative = _try(() => getChainAddresses(params.chainId).wNative ?? null, UnsupportedChainIdError);
73
- const simulationTxs = buildSimulationTxs(params);
74
- const result = await executeSimulation({
75
- config,
76
- chainId: params.chainId,
77
- transactions: simulationTxs,
78
- blockNumber: params.blockNumber,
79
- wNative,
80
- });
81
- if (result.calls.length !== simulationTxs.length) {
82
- throw new ExternalServiceError(`Backend returned ${result.calls.length} call result(s) for ${simulationTxs.length} transaction(s) — refusing to map transfers with mismatched lengths`);
83
- }
84
- const transfers = parseTransfers(result.calls, {
85
- wNative,
86
- logger: config.logger,
87
- });
88
- assertNoBundlesRetention({
89
- chainId: params.chainId,
90
- transfers,
91
- assetChanges: result.assetChanges,
92
- logger: config.logger,
93
- });
94
- return {
95
- simulationTxs,
96
- calls: result.calls,
97
- transfers,
98
- assetChanges: result.assetChanges,
99
- };
117
+ return runSimulation({ config, request: parseRequest(params) });
100
118
  }
@@ -0,0 +1,18 @@
1
+ import { type SimulationVerificationContext } from "../errors.js";
2
+ import type { SlippageLimits, SlippageQuote } from "../limits.js";
3
+ /**
4
+ * Compare observed amounts against the caller's quote and percentage tolerance.
5
+ * Minimum outputs round up and maximum inputs round down, so rounding never
6
+ * permits a deviation greater than the requested percentage.
7
+ * @param params - Parsed quote/tolerance, matching observations, and error context.
8
+ * @returns The checked quote and tolerance, deep-frozen.
9
+ * @throws {ConsumerLimitViolationError} When adverse slippage exceeds tolerance.
10
+ * @throws {MissingVerificationEvidenceError} When a quoted amount has no observation.
11
+ * @internal
12
+ */
13
+ export declare function checkSlippage(params: {
14
+ readonly limits: SlippageLimits;
15
+ readonly observed: SlippageQuote;
16
+ readonly context: SimulationVerificationContext;
17
+ readonly debtShares?: boolean;
18
+ }): SlippageLimits;