@morpho-org/evm-simulation 4.2.1 → 5.0.1

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 +26 -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 +69 -20
  23. package/lib/cjs/simulate/backends/eth-simulate-v1.js +174 -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 +156 -0
  54. package/lib/cjs/simulate/simulate.d.ts +102 -51
  55. package/lib/cjs/simulate/simulate.js +101 -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 +37 -96
  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 +26 -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 +69 -20
  86. package/lib/esm/simulate/backends/eth-simulate-v1.js +172 -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 +153 -0
  117. package/lib/esm/simulate/simulate.d.ts +102 -51
  118. package/lib/esm/simulate/simulate.js +101 -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 +37 -96
  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,153 @@
1
+ import { ChainId, 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
+ // Monad nodes reject `validation: false` and charge no gas in the simulated
52
+ // block either way. Its `latest` block is not final: simulating at its number
53
+ // runs on another parent, so default to `finalized`.
54
+ const monad = request.chainId === ChainId.MonadMainnet;
55
+ const pinnedBlock = await resolvePinnedBlock({
56
+ client,
57
+ blockNumber: request.blockNumber ?? (monad ? "finalized" : undefined),
58
+ signal,
59
+ });
60
+ // With no calldata decoding, the caller's declared `limits.operations`
61
+ // entries are the operations: subjects for state reads and per-operation
62
+ // checks come straight from the limit list.
63
+ const resolved = await resolveAssets({
64
+ client,
65
+ morpho,
66
+ operations: limits.operations,
67
+ chainId: request.chainId,
68
+ mode: request.mode,
69
+ blockNumber: pinnedBlock.number,
70
+ });
71
+ const observations = planStateReads({ operations: resolved, owner, morpho });
72
+ const preparations = preview
73
+ ? prepareAuthorizations({
74
+ authorizations: request.authorizations,
75
+ owner,
76
+ morpho,
77
+ })
78
+ : [];
79
+ const plan = planExecution({
80
+ request,
81
+ owner,
82
+ preparations,
83
+ reads: observations.reads,
84
+ });
85
+ // Execute against the block resolved once above; the boundary rechecks its hash.
86
+ const execution = await executePlan({
87
+ client,
88
+ plan,
89
+ stateBlock: pinnedBlock,
90
+ validation: monad,
91
+ });
92
+ const userCalls = execution.calls
93
+ .filter((call) => call.planned.type === "transaction")
94
+ .map((call) => call.result);
95
+ if (userCalls.length !== request.transactions.length)
96
+ throw new InvalidSimulationResponseError(`Execution returned ${userCalls.length} user call result(s) for ${request.transactions.length} transaction(s)`);
97
+ const decodePhase = (phase) => new Map(execution.stateReads
98
+ .filter((read) => read.phase === phase)
99
+ .map((read) => [
100
+ read.read.id,
101
+ decodeStateRead(read.read, read.returnData, {
102
+ chainId: request.chainId,
103
+ mode: request.mode,
104
+ blockNumber: pinnedBlock.number,
105
+ }),
106
+ ]));
107
+ const transfers = parseTransfers(userCalls, {
108
+ wNative: addresses === undefined ? undefined : (addresses.wNative ?? null),
109
+ logger: config.logger,
110
+ });
111
+ const assetChanges = groupAssetChanges(transfers.flatMap(({ token, from, to, amount }) => [
112
+ { account: to, token, diff: amount },
113
+ { account: from, token, diff: -amount },
114
+ ]));
115
+ const ctx = {
116
+ chainId: request.chainId,
117
+ mode: request.mode,
118
+ block: execution.block,
119
+ owner,
120
+ limits,
121
+ };
122
+ const authorizations = request.authorizations.map((authorization, authorizationIndex) => ({
123
+ authorizationIndex,
124
+ authorization,
125
+ calls: execution.calls.flatMap(({ planned, result }) => planned.type === "preparation" &&
126
+ planned.authorizationIndex === authorizationIndex
127
+ ? [{ transaction: planned.transaction, result }]
128
+ : []),
129
+ }));
130
+ const checked = verifySlippage({
131
+ ctx,
132
+ operations: observations.operations,
133
+ before: decodePhase("before"),
134
+ after: decodePhase("after"),
135
+ transfers,
136
+ requestTransactions: request.transactions,
137
+ });
138
+ assertNoBundlesRetention({
139
+ chainId: request.chainId,
140
+ transfers,
141
+ assetChanges,
142
+ logger: config.logger,
143
+ });
144
+ return assembleResult({
145
+ ctx,
146
+ request,
147
+ operations: checked,
148
+ authorizations,
149
+ userCalls,
150
+ transfers,
151
+ assetChanges,
152
+ });
153
+ }
@@ -1,68 +1,119 @@
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 the restricted standalone bundles contracts (VaultExitBundlesV1, VaultBundlesV1,
11
- * BlueBundlesV1, MidnightBundlesV1) → returns the full result
12
- * 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:
13
15
  *
14
- * - `transfers` → user-facing preview / server-side verification.
15
- * - `simulationTxs` + `transfers` → server-side verification before broadcast.
16
- * - `calls[i]` → per-tx raw backend output (`logs`, `status`, `returnData`, `gasUsed`).
17
- * Aligned 1:1 with `simulationTxs[i]`. `gasUsed` is not a safe gas limit; consumers
18
- * deriving one must add their own headroom.
19
- * - `assetChanges` → net per-asset balance changes grouped by account (sender and
20
- * counterparties) over the whole bundle, normalized to the same shape across backends.
21
- * - `transfers[k].txIdx` → index into `simulationTxs` of the tx that emitted the
22
- * 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.
23
26
  *
24
- * @param config - Backend configuration: per-chain Tenderly RPC and/or `eth_simulateV1`
25
- * 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. Monad nodes reject `false`, so Monad
38
+ * simulations send `true`; its simulated block charges no gas either way.
39
+ *
40
+ * @param config - Required per-chain `eth_simulateV1` URL, optional logger, and
41
+ * the overall timeout budget.
26
42
  * @param params - Per-call simulation input.
27
- * @param params.chainId - Chain id the bundle targets.
28
- * @param params.transactions - The bundle's transactions, in execution order. All must share the
29
- * same `from`.
30
- * @param params.authorizations - Optional token authorizations resolved into prepended approve
31
- * transactions before the main bundle runs.
32
- * @param params.blockNumber - Optional pinned block number or `BlockTag`. Defaults to `latest`.
33
- * @throws {SimulationValidationError} for invalid input (mixed senders, bad addresses,
34
- * empty transactions, malformed authorizations).
35
- * @throws {UnsupportedChainError} when the chain is not configured for any backend.
36
- * @throws {SimulationRevertedError} when the bundle reverts on either backend.
37
- * @throws {BlacklistViolationError} when the simulation leaves value retained beyond
38
- * the dust threshold by a `bundles` periphery contract (VaultExitBundlesV1,
39
- * VaultBundlesV1, BlueBundlesV1, MidnightBundlesV1). Never bypassable.
40
- * @throws {ExternalServiceError} (a) when both backends are unavailable within the
41
- * timeout budget, or (b) when a backend returns a `calls` array whose length does
42
- * not match the resolved `simulationTxs` — refusing to map transfers with mismatched
43
- * per-tx output.
44
- * @returns A {@link SimulationResult} carrying the resolved `simulationTxs`, per-tx
45
- * `calls` (aligned 1:1 with `simulationTxs`), parsed `transfers` (each stamped
46
- * with `txIdx`), and per-account net `assetChanges`.
43
+ * @param params.chainId - Chain id the bundle targets; must match the endpoint.
44
+ * @param params.transactions - The bundle's transactions, in execution order.
45
+ * All must share the same `from`.
46
+ * @param params.mode - `"final"` (default) or `"preview"`.
47
+ * @param params.authorizations - Preview-only typed authorization descriptors.
48
+ * @param params.limits - Optional quotes and percentage tolerances; unquoted amounts require no reads.
49
+ * @param params.blockNumber - Optional pinned block number or `BlockTag` other
50
+ * than `"pending"`.
51
+ * Defaults to `latest` (`finalized` on Monad, whose `latest` block is not
52
+ * final), resolved exactly once.
53
+ * @throws {SimulationValidationError} for invalid input (mixed senders, bad
54
+ * addresses, empty transactions, malformed authorizations, final-mode
55
+ * authorizations, malformed limits, unknown fields, a `"pending"` block tag, or share quotes for
56
+ * `blueSupplyCollateral` / `blueWithdrawCollateral`).
57
+ * @throws {ConsumerLimitViolationError} when a declared `limits` bound is
58
+ * violated by the observed effects.
59
+ * @throws {UnsupportedChainError} when the chain has no `eth_simulateV1`
60
+ * endpoint configured, or limits/preview authorizations require a Morpho
61
+ * Blue address absent from blue-sdk's `getChainAddresses`.
62
+ * @throws {SimulationRevertedError} when a preparation or user transaction reverts.
63
+ * @throws {MissingVerificationEvidenceError} when a planned state read fails
64
+ * or returns empty data, native outgoing traces do not cover value
65
+ * sent, or required metadata reverts or returns empty/invalid data.
66
+ * @throws {InvalidSimulationResponseError} when the node response cannot be
67
+ * trusted (bad shape, call-count mismatch, block that is neither the pinned
68
+ * state block nor its immediate successor, a successor with a mismatched
69
+ * `parentHash`, a block timestamp earlier than the pinned block's, a
70
+ * malformed per-call result, a quoted balance/position read whose non-empty
71
+ * return data cannot be decoded, a state-block hash that changed or a pinned
72
+ * block that vanished mid-flight, or an
73
+ * endpoint whose `eth_chainId` differs from `params.chainId`; chain identity
74
+ * is checked before any block lookup).
75
+ * @throws {BlacklistViolationError} when the simulation leaves value retained
76
+ * beyond the dust threshold by a `bundles` periphery contract
77
+ * (VaultExitBundlesV1, VaultBundlesV1, BlueBundlesV1, MidnightBundlesV1).
78
+ * Never bypassable.
79
+ * @throws {ExternalServiceError} when the RPC is unavailable within the
80
+ * timeout budget or returns a malformed JSON-RPC envelope. Chain-id
81
+ * mismatches and a state block without number/hash are reported as
82
+ * `InvalidSimulationResponseError`.
83
+ * @returns A frozen {@link VerifiedSimulationResult} carrying the normalized
84
+ * `simulationTxs`, per-tx `calls` (aligned 1:1), parsed `transfers` (each
85
+ * stamped with `txIdx`), per-account net `assetChanges`, and per-operation
86
+ * verification outcomes and preparations. Each `verification.operations[i]`
87
+ * mirrors the `limits.operations[i]` entry it verified. Only the amounts
88
+ * quoted in each entry are checked; `checkedLimits` identifies precisely
89
+ * what was checked. Measurements are scoped to the whole bundle.
47
90
  * @example
48
91
  * ```ts
49
92
  * import { simulate } from "@morpho-org/evm-simulation";
93
+ * import { type Address, encodeFunctionData, erc20Abi, getAddress } from "viem";
94
+ *
95
+ * const rpcUrl = "https://rpc.example";
96
+ * const user: Address = getAddress("0x1111111111111111111111111111111111111111");
97
+ * const usdc: Address = getAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
98
+ * const recipient: Address = getAddress("0x2222222222222222222222222222222222222222");
50
99
  *
51
100
  * const result = await simulate(
52
- * {
53
- * chains: new Map([
54
- * [1, {
55
- * tenderlyRpc: { rpcUrl: process.env.TENDERLY_RPC_URL! },
56
- * simulateV1Url: process.env.MAINNET_RPC_URL,
57
- * }],
58
- * ]),
59
- * },
101
+ * { chains: new Map([[1, { simulateV1Url: rpcUrl }]]) },
60
102
  * {
61
103
  * chainId: 1,
62
- * transactions: [{ from: user, to: vaultAddress, data: encodedCalldata, value: 0n }],
104
+ * transactions: [
105
+ * {
106
+ * from: user,
107
+ * to: usdc,
108
+ * data: encodeFunctionData({
109
+ * abi: erc20Abi,
110
+ * functionName: "transfer",
111
+ * args: [recipient, 1_000_000n],
112
+ * }),
113
+ * },
114
+ * ],
63
115
  * },
64
116
  * );
65
- * // result satisfies SimulationResult
66
117
  * ```
67
118
  */
68
- export declare function simulate(config: SimulationConfig, params: SimulateParams): Promise<SimulationResult>;
119
+ export declare function simulate(config: SimulationConfig, params: SimulateParams): Promise<VerifiedSimulationResult>;
@@ -1,101 +1,120 @@
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 the restricted standalone bundles contracts (VaultExitBundlesV1, VaultBundlesV1,
14
- * BlueBundlesV1, MidnightBundlesV1) → returns the full result
15
- * 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:
16
14
  *
17
- * - `transfers` → user-facing preview / server-side verification.
18
- * - `simulationTxs` + `transfers` → server-side verification before broadcast.
19
- * - `calls[i]` → per-tx raw backend output (`logs`, `status`, `returnData`, `gasUsed`).
20
- * Aligned 1:1 with `simulationTxs[i]`. `gasUsed` is not a safe gas limit; consumers
21
- * deriving one must add their own headroom.
22
- * - `assetChanges` → net per-asset balance changes grouped by account (sender and
23
- * counterparties) over the whole bundle, normalized to the same shape across backends.
24
- * - `transfers[k].txIdx` → index into `simulationTxs` of the tx that emitted the
25
- * 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.
26
25
  *
27
- * @param config - Backend configuration: per-chain Tenderly RPC and/or `eth_simulateV1`
28
- * 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. Monad nodes reject `false`, so Monad
37
+ * simulations send `true`; its simulated block charges no gas either way.
38
+ *
39
+ * @param config - Required per-chain `eth_simulateV1` URL, optional logger, and
40
+ * the overall timeout budget.
29
41
  * @param params - Per-call simulation input.
30
- * @param params.chainId - Chain id the bundle targets.
31
- * @param params.transactions - The bundle's transactions, in execution order. All must share the
32
- * same `from`.
33
- * @param params.authorizations - Optional token authorizations resolved into prepended approve
34
- * transactions before the main bundle runs.
35
- * @param params.blockNumber - Optional pinned block number or `BlockTag`. Defaults to `latest`.
36
- * @throws {SimulationValidationError} for invalid input (mixed senders, bad addresses,
37
- * empty transactions, malformed authorizations).
38
- * @throws {UnsupportedChainError} when the chain is not configured for any backend.
39
- * @throws {SimulationRevertedError} when the bundle reverts on either backend.
40
- * @throws {BlacklistViolationError} when the simulation leaves value retained beyond
41
- * the dust threshold by a `bundles` periphery contract (VaultExitBundlesV1,
42
- * VaultBundlesV1, BlueBundlesV1, MidnightBundlesV1). Never bypassable.
43
- * @throws {ExternalServiceError} (a) when both backends are unavailable within the
44
- * timeout budget, or (b) when a backend returns a `calls` array whose length does
45
- * not match the resolved `simulationTxs` — refusing to map transfers with mismatched
46
- * per-tx output.
47
- * @returns A {@link SimulationResult} carrying the resolved `simulationTxs`, per-tx
48
- * `calls` (aligned 1:1 with `simulationTxs`), parsed `transfers` (each stamped
49
- * 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` (`finalized` on Monad, whose `latest` block is not
51
+ * final), resolved exactly once.
52
+ * @throws {SimulationValidationError} for invalid input (mixed senders, bad
53
+ * addresses, empty transactions, malformed authorizations, final-mode
54
+ * authorizations, malformed limits, unknown fields, a `"pending"` block tag, or share quotes for
55
+ * `blueSupplyCollateral` / `blueWithdrawCollateral`).
56
+ * @throws {ConsumerLimitViolationError} when a declared `limits` bound is
57
+ * violated by the observed effects.
58
+ * @throws {UnsupportedChainError} when the chain has no `eth_simulateV1`
59
+ * endpoint configured, or limits/preview authorizations require a Morpho
60
+ * Blue address absent from blue-sdk's `getChainAddresses`.
61
+ * @throws {SimulationRevertedError} when a preparation or user transaction reverts.
62
+ * @throws {MissingVerificationEvidenceError} when a planned state read fails
63
+ * or returns empty data, native outgoing traces do not cover value
64
+ * sent, or required metadata reverts or returns empty/invalid data.
65
+ * @throws {InvalidSimulationResponseError} when the node response cannot be
66
+ * trusted (bad shape, call-count mismatch, block that is neither the pinned
67
+ * state block nor its immediate successor, a successor with a mismatched
68
+ * `parentHash`, a block timestamp earlier than the pinned block's, a
69
+ * malformed per-call result, a quoted balance/position read whose non-empty
70
+ * return data cannot be decoded, a state-block hash that changed or a pinned
71
+ * block that vanished mid-flight, or an
72
+ * endpoint whose `eth_chainId` differs from `params.chainId`; chain identity
73
+ * is checked before any block lookup).
74
+ * @throws {BlacklistViolationError} when the simulation leaves value retained
75
+ * beyond the dust threshold by a `bundles` periphery contract
76
+ * (VaultExitBundlesV1, VaultBundlesV1, BlueBundlesV1, MidnightBundlesV1).
77
+ * Never bypassable.
78
+ * @throws {ExternalServiceError} when the RPC is unavailable within the
79
+ * timeout budget or returns a malformed JSON-RPC envelope. Chain-id
80
+ * mismatches and a state block without number/hash are reported as
81
+ * `InvalidSimulationResponseError`.
82
+ * @returns A frozen {@link VerifiedSimulationResult} carrying the normalized
83
+ * `simulationTxs`, per-tx `calls` (aligned 1:1), parsed `transfers` (each
84
+ * stamped with `txIdx`), per-account net `assetChanges`, and per-operation
85
+ * verification outcomes and preparations. Each `verification.operations[i]`
86
+ * mirrors the `limits.operations[i]` entry it verified. Only the amounts
87
+ * quoted in each entry are checked; `checkedLimits` identifies precisely
88
+ * what was checked. Measurements are scoped to the whole bundle.
50
89
  * @example
51
90
  * ```ts
52
91
  * import { simulate } from "@morpho-org/evm-simulation";
92
+ * import { type Address, encodeFunctionData, erc20Abi, getAddress } from "viem";
93
+ *
94
+ * const rpcUrl = "https://rpc.example";
95
+ * const user: Address = getAddress("0x1111111111111111111111111111111111111111");
96
+ * const usdc: Address = getAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
97
+ * const recipient: Address = getAddress("0x2222222222222222222222222222222222222222");
53
98
  *
54
99
  * const result = await simulate(
55
- * {
56
- * chains: new Map([
57
- * [1, {
58
- * tenderlyRpc: { rpcUrl: process.env.TENDERLY_RPC_URL! },
59
- * simulateV1Url: process.env.MAINNET_RPC_URL,
60
- * }],
61
- * ]),
62
- * },
100
+ * { chains: new Map([[1, { simulateV1Url: rpcUrl }]]) },
63
101
  * {
64
102
  * chainId: 1,
65
- * transactions: [{ from: user, to: vaultAddress, data: encodedCalldata, value: 0n }],
103
+ * transactions: [
104
+ * {
105
+ * from: user,
106
+ * to: usdc,
107
+ * data: encodeFunctionData({
108
+ * abi: erc20Abi,
109
+ * functionName: "transfer",
110
+ * args: [recipient, 1_000_000n],
111
+ * }),
112
+ * },
113
+ * ],
66
114
  * },
67
115
  * );
68
- * // result satisfies SimulationResult
69
116
  * ```
70
117
  */
71
118
  export async function simulate(config, params) {
72
- validateInput(params);
73
- const wNative = _try(() => getChainAddresses(params.chainId).wNative ?? null, UnsupportedChainIdError);
74
- const simulationTxs = buildSimulationTxs(params);
75
- const result = await executeSimulation({
76
- config,
77
- chainId: params.chainId,
78
- transactions: simulationTxs,
79
- blockNumber: params.blockNumber,
80
- wNative,
81
- });
82
- if (result.calls.length !== simulationTxs.length) {
83
- throw new ExternalServiceError(`Backend returned ${result.calls.length} call result(s) for ${simulationTxs.length} transaction(s) — refusing to map transfers with mismatched lengths`);
84
- }
85
- const transfers = parseTransfers(result.calls, {
86
- wNative,
87
- logger: config.logger,
88
- });
89
- assertNoBundlesRetention({
90
- chainId: params.chainId,
91
- transfers,
92
- assetChanges: result.assetChanges,
93
- logger: config.logger,
94
- });
95
- return {
96
- simulationTxs,
97
- calls: result.calls,
98
- transfers,
99
- assetChanges: result.assetChanges,
100
- };
119
+ return runSimulation({ config, request: parseRequest(params) });
101
120
  }
@@ -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;