@morpho-org/evm-simulation 2.0.2 → 4.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 (59) hide show
  1. package/README.md +22 -30
  2. package/lib/cjs/errors.d.ts +0 -6
  3. package/lib/cjs/errors.js +1 -11
  4. package/lib/cjs/index.d.ts +2 -3
  5. package/lib/cjs/index.js +3 -6
  6. package/lib/cjs/simulate/asset-changes.d.ts +19 -0
  7. package/lib/cjs/simulate/asset-changes.js +47 -0
  8. package/lib/cjs/simulate/backends/eth-simulate-v1.d.ts +9 -0
  9. package/lib/cjs/simulate/backends/eth-simulate-v1.js +39 -1
  10. package/lib/cjs/simulate/backends/index.d.ts +1 -1
  11. package/lib/cjs/simulate/backends/index.js +3 -3
  12. package/lib/cjs/simulate/backends/tenderly-rpc.d.ts +23 -0
  13. package/lib/cjs/simulate/backends/tenderly-rpc.js +230 -0
  14. package/lib/cjs/simulate/parsing/transfers.js +3 -1
  15. package/lib/cjs/simulate/pipeline/execute-simulation.d.ts +6 -10
  16. package/lib/cjs/simulate/pipeline/execute-simulation.js +11 -18
  17. package/lib/cjs/simulate/pipeline/resolve-chain.d.ts +6 -5
  18. package/lib/cjs/simulate/pipeline/resolve-chain.js +10 -7
  19. package/lib/cjs/simulate/simulate.d.ts +20 -23
  20. package/lib/cjs/simulate/simulate.js +21 -20
  21. package/lib/cjs/types.d.ts +58 -39
  22. package/lib/esm/errors.d.ts +0 -6
  23. package/lib/esm/errors.js +0 -9
  24. package/lib/esm/index.d.ts +2 -3
  25. package/lib/esm/index.js +1 -2
  26. package/lib/esm/simulate/asset-changes.d.ts +19 -0
  27. package/lib/esm/simulate/asset-changes.js +44 -0
  28. package/lib/esm/simulate/backends/eth-simulate-v1.d.ts +9 -0
  29. package/lib/esm/simulate/backends/eth-simulate-v1.js +40 -2
  30. package/lib/esm/simulate/backends/index.d.ts +1 -1
  31. package/lib/esm/simulate/backends/index.js +1 -1
  32. package/lib/esm/simulate/backends/tenderly-rpc.d.ts +23 -0
  33. package/lib/esm/simulate/backends/tenderly-rpc.js +227 -0
  34. package/lib/esm/simulate/parsing/transfers.js +3 -1
  35. package/lib/esm/simulate/pipeline/execute-simulation.d.ts +6 -10
  36. package/lib/esm/simulate/pipeline/execute-simulation.js +12 -19
  37. package/lib/esm/simulate/pipeline/resolve-chain.d.ts +6 -5
  38. package/lib/esm/simulate/pipeline/resolve-chain.js +10 -7
  39. package/lib/esm/simulate/simulate.d.ts +20 -23
  40. package/lib/esm/simulate/simulate.js +21 -20
  41. package/lib/esm/types.d.ts +58 -39
  42. package/package.json +7 -5
  43. package/src/index.ts +3 -3
  44. package/lib/cjs/screen-addresses/index.d.ts +0 -1
  45. package/lib/cjs/screen-addresses/index.js +0 -5
  46. package/lib/cjs/screen-addresses/sanctioned-addresses.d.ts +0 -11
  47. package/lib/cjs/screen-addresses/sanctioned-addresses.js +0 -77
  48. package/lib/cjs/screen-addresses/screen-addresses.d.ts +0 -50
  49. package/lib/cjs/screen-addresses/screen-addresses.js +0 -171
  50. package/lib/cjs/simulate/backends/tenderly-rest.d.ts +0 -20
  51. package/lib/cjs/simulate/backends/tenderly-rest.js +0 -279
  52. package/lib/esm/screen-addresses/index.d.ts +0 -1
  53. package/lib/esm/screen-addresses/index.js +0 -1
  54. package/lib/esm/screen-addresses/sanctioned-addresses.d.ts +0 -11
  55. package/lib/esm/screen-addresses/sanctioned-addresses.js +0 -74
  56. package/lib/esm/screen-addresses/screen-addresses.d.ts +0 -50
  57. package/lib/esm/screen-addresses/screen-addresses.js +0 -168
  58. package/lib/esm/simulate/backends/tenderly-rest.d.ts +0 -20
  59. package/lib/esm/simulate/backends/tenderly-rest.js +0 -276
@@ -1,2 +1,2 @@
1
1
  export { simulateV1 } from "./eth-simulate-v1.js";
2
- export { simulateTenderlyRest } from "./tenderly-rest.js";
2
+ export { simulateTenderlyRpc } from "./tenderly-rpc.js";
@@ -0,0 +1,23 @@
1
+ import { type BlockTag } from "viem";
2
+ import type { RawSimulationResult, SimulationTransaction, TenderlyRpcConfig } from "../../types.js";
3
+ /**
4
+ * Simulate one or more transactions via Tenderly's Node Web3 Gateway.
5
+ * Dispatches to `tenderly_simulateTransaction` for a single call and
6
+ * `tenderly_simulateBundle` for a multi-call bundle.
7
+ *
8
+ * @param params.config - Tenderly RPC config (URL with access key embedded).
9
+ * @param params.transactions - Bundle to simulate, in execution order.
10
+ * @param params.blockNumber - Optional pinned block number or `BlockTag`. Defaults to `latest`.
11
+ * @param params.signal - Optional `AbortSignal` for cancellation / timeout.
12
+ * @returns A {@link RawSimulationResult} with one `RawCall` per input transaction.
13
+ * @throws {SimulationValidationError} when `transactions` is empty.
14
+ * @throws {SimulationRevertedError} when any simulated tx reports `status: false`.
15
+ * @throws {ExternalServiceError} on non-2xx response, JSON-RPC envelope error,
16
+ * schema-validation failure, or fetch-layer failure.
17
+ */
18
+ export declare function simulateTenderlyRpc(params: {
19
+ config: TenderlyRpcConfig;
20
+ transactions: SimulationTransaction[];
21
+ blockNumber?: bigint | BlockTag;
22
+ signal?: AbortSignal;
23
+ }): Promise<RawSimulationResult>;
@@ -0,0 +1,227 @@
1
+ import { ethAddress, getAddress, isAddress, isHex, maxUint256, numberToHex, } from "viem";
2
+ import { z } from "zod";
3
+ import { ExternalServiceError, SimulationRevertedError, SimulationValidationError, } from "../../errors.js";
4
+ import { groupAssetChanges } from "../asset-changes.js";
5
+ const addressSchema = z.custom((val) => typeof val === "string" && isAddress(val));
6
+ const hexSchema = z.custom((val) => typeof val === "string" && isHex(val));
7
+ const traceFrameSchema = z
8
+ .object({
9
+ output: hexSchema.optional(),
10
+ error: z.string().optional(),
11
+ errorReason: z.string().optional(),
12
+ })
13
+ .passthrough();
14
+ const logSchema = z
15
+ .object({
16
+ raw: z
17
+ .object({
18
+ address: addressSchema,
19
+ topics: z.array(hexSchema).optional(),
20
+ data: hexSchema.optional(),
21
+ })
22
+ .passthrough()
23
+ .optional(),
24
+ })
25
+ .passthrough();
26
+ const assetChangeSchema = z
27
+ .object({
28
+ assetInfo: z
29
+ .object({
30
+ contractAddress: addressSchema.optional(),
31
+ symbol: z.string().optional(),
32
+ decimals: z.number().optional(),
33
+ })
34
+ .passthrough(),
35
+ from: addressSchema.optional(),
36
+ to: addressSchema.optional(),
37
+ rawAmount: hexSchema,
38
+ })
39
+ .passthrough();
40
+ const simResultSchema = z
41
+ .object({
42
+ status: z.boolean(),
43
+ gasUsed: hexSchema,
44
+ logs: z.array(logSchema).optional(),
45
+ trace: z.array(traceFrameSchema).optional(),
46
+ assetChanges: z.array(assetChangeSchema).optional(),
47
+ error: z.string().optional(),
48
+ errorMessage: z.string().optional(),
49
+ })
50
+ .passthrough();
51
+ const rpcErrorSchema = z
52
+ .object({
53
+ code: z.number().optional(),
54
+ message: z.string(),
55
+ data: z.unknown().optional(),
56
+ })
57
+ .passthrough();
58
+ function rpcEnvelope(result) {
59
+ return z
60
+ .object({ result: result.optional(), error: rpcErrorSchema.optional() })
61
+ .passthrough();
62
+ }
63
+ const singleEnvelope = rpcEnvelope(simResultSchema);
64
+ const bundleEnvelope = rpcEnvelope(z.array(simResultSchema).min(1));
65
+ /**
66
+ * Simulate one or more transactions via Tenderly's Node Web3 Gateway.
67
+ * Dispatches to `tenderly_simulateTransaction` for a single call and
68
+ * `tenderly_simulateBundle` for a multi-call bundle.
69
+ *
70
+ * @param params.config - Tenderly RPC config (URL with access key embedded).
71
+ * @param params.transactions - Bundle to simulate, in execution order.
72
+ * @param params.blockNumber - Optional pinned block number or `BlockTag`. Defaults to `latest`.
73
+ * @param params.signal - Optional `AbortSignal` for cancellation / timeout.
74
+ * @returns A {@link RawSimulationResult} with one `RawCall` per input transaction.
75
+ * @throws {SimulationValidationError} when `transactions` is empty.
76
+ * @throws {SimulationRevertedError} when any simulated tx reports `status: false`.
77
+ * @throws {ExternalServiceError} on non-2xx response, JSON-RPC envelope error,
78
+ * schema-validation failure, or fetch-layer failure.
79
+ */
80
+ export async function simulateTenderlyRpc(params) {
81
+ const { config, transactions, blockNumber, signal } = params;
82
+ const firstTx = transactions[0];
83
+ if (!firstTx) {
84
+ throw new SimulationValidationError("At least one transaction is required", []);
85
+ }
86
+ const block = encodeBlock(blockNumber);
87
+ // Inflate sender ETH balance to avoid false "insufficient funds for gas"
88
+ // reverts on wallets low on native gas token — mirrors simulateV1.
89
+ const stateOverrides = buildStateOverrides(firstTx.from);
90
+ try {
91
+ if (transactions.length === 1) {
92
+ const json = await rpcRequest({
93
+ rpcUrl: config.rpcUrl,
94
+ method: "tenderly_simulateTransaction",
95
+ params: [buildCall(firstTx), block, stateOverrides],
96
+ signal,
97
+ });
98
+ const result = unwrapResult(singleEnvelope.parse(json));
99
+ return {
100
+ calls: [toRawCall(result)],
101
+ assetChanges: toAssetChanges([result]),
102
+ };
103
+ }
104
+ const json = await rpcRequest({
105
+ rpcUrl: config.rpcUrl,
106
+ method: "tenderly_simulateBundle",
107
+ params: [transactions.map(buildCall), block, stateOverrides],
108
+ signal,
109
+ });
110
+ const results = unwrapResult(bundleEnvelope.parse(json));
111
+ return {
112
+ calls: results.map(toRawCall),
113
+ assetChanges: toAssetChanges(results),
114
+ };
115
+ }
116
+ catch (error) {
117
+ if (error instanceof SimulationRevertedError)
118
+ throw error;
119
+ if (error instanceof ExternalServiceError)
120
+ throw error;
121
+ throw new ExternalServiceError(`Tenderly RPC error: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
122
+ }
123
+ }
124
+ async function rpcRequest(params) {
125
+ const { rpcUrl, method, params: rpcParams, signal } = params;
126
+ const response = await fetch(rpcUrl, {
127
+ method: "POST",
128
+ headers: { "Content-Type": "application/json" },
129
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params: rpcParams }),
130
+ signal,
131
+ });
132
+ if (!response.ok) {
133
+ throw new ExternalServiceError(`Tenderly RPC returned ${response.status}: ${response.statusText}`);
134
+ }
135
+ return await response.json();
136
+ }
137
+ function unwrapResult(envelope) {
138
+ if (envelope.error) {
139
+ throw new ExternalServiceError(`Tenderly RPC error: ${envelope.error.message}`);
140
+ }
141
+ if (envelope.result === undefined) {
142
+ throw new ExternalServiceError("Tenderly RPC returned no result");
143
+ }
144
+ return envelope.result;
145
+ }
146
+ function buildCall(tx) {
147
+ return {
148
+ from: tx.from,
149
+ to: tx.to,
150
+ input: tx.data,
151
+ data: tx.data,
152
+ value: numberToHex(tx.value ?? 0n),
153
+ };
154
+ }
155
+ function buildStateOverrides(sender) {
156
+ return { [sender]: { balance: numberToHex(maxUint256) } };
157
+ }
158
+ function encodeBlock(blockNumber) {
159
+ if (blockNumber === undefined)
160
+ return "latest";
161
+ return typeof blockNumber === "bigint"
162
+ ? numberToHex(blockNumber)
163
+ : blockNumber;
164
+ }
165
+ function toRawCall(data) {
166
+ if (data.status !== true) {
167
+ // Tenderly RPC surfaces the revert reason on the trace frame; the
168
+ // top-level fields are checked as a defensive fallback.
169
+ const traceError = data.trace?.find((f) => f.errorReason || f.error);
170
+ const message = traceError?.errorReason ||
171
+ traceError?.error ||
172
+ data.errorMessage ||
173
+ data.error ||
174
+ "Transaction simulation reverted";
175
+ throw new SimulationRevertedError(message, data);
176
+ }
177
+ const logs = [];
178
+ for (const log of data.logs ?? []) {
179
+ if (log.raw) {
180
+ logs.push({
181
+ address: log.raw.address,
182
+ topics: log.raw.topics ?? [],
183
+ data: log.raw.data ?? "0x",
184
+ });
185
+ }
186
+ }
187
+ return {
188
+ logs,
189
+ status: true,
190
+ returnData: data.trace?.[0]?.output ?? "0x",
191
+ gasUsed: BigInt(data.gasUsed),
192
+ };
193
+ }
194
+ /**
195
+ * Reduce Tenderly's per-transfer asset changes to net per-token balance changes
196
+ * grouped by account. Both endpoints of each transfer are accounted (sender,
197
+ * counterparties, and the zero address for mints/burns).
198
+ */
199
+ function toAssetChanges(results) {
200
+ const entries = [];
201
+ for (const result of results) {
202
+ for (const change of result.assetChanges ?? []) {
203
+ const amount = BigInt(change.rawAmount);
204
+ const token = change.assetInfo.contractAddress
205
+ ? getAddress(change.assetInfo.contractAddress)
206
+ : ethAddress;
207
+ const { symbol, decimals } = change.assetInfo;
208
+ if (change.to)
209
+ entries.push({
210
+ account: change.to,
211
+ token,
212
+ diff: amount,
213
+ symbol,
214
+ decimals,
215
+ });
216
+ if (change.from)
217
+ entries.push({
218
+ account: change.from,
219
+ token,
220
+ diff: -amount,
221
+ symbol,
222
+ decimals,
223
+ });
224
+ }
225
+ }
226
+ return groupAssetChanges(entries);
227
+ }
@@ -141,7 +141,9 @@ export function parseTransfers(calls, logger) {
141
141
  catch (error) {
142
142
  // Safety net for anything that slips past the length checks above
143
143
  // (e.g., viem checksum throws on non-hex input). Loud enough to diagnose.
144
- warnMalformed(logger, log, error instanceof Error ? error.message : String(error));
144
+ warnMalformed(logger, log,
145
+ /* v8 ignore next: parser paths throw Error instances for valid RawLog shapes. */
146
+ error instanceof Error ? error.message : String(error));
145
147
  }
146
148
  }
147
149
  }
@@ -5,21 +5,17 @@ import type { RawSimulationResult, SimulationConfig, SimulationTransaction } fro
5
5
  *
6
6
  * Dispatches the transaction bundle to the available backend with a shared timeout
7
7
  * budget:
8
- * - If Tenderly is configured for the chain, it gets `TENDERLY_BUDGET_RATIO` (60%) of
9
- * the timeout. On `ExternalServiceError`, falls back to `eth_simulateV1` with
10
- * `max(remaining, FALLBACK_MIN_BUDGET_MS)` so the fallback still has a viable
11
- * window when Tenderly eats its full slice. `SimulationRevertedError` (contract
12
- * revert) propagates immediately without retry — a revert is a property of the
13
- * bundle, not the backend.
8
+ * - If a Tenderly RPC URL is configured for the chain, it gets
9
+ * `TENDERLY_BUDGET_RATIO` (60%) of the timeout. On `ExternalServiceError`,
10
+ * falls back to `eth_simulateV1` with `max(remaining, FALLBACK_MIN_BUDGET_MS)`
11
+ * so the fallback still has a viable window when Tenderly eats its full slice.
12
+ * `SimulationRevertedError` (contract revert) propagates immediately without
13
+ * retry — a revert is a property of the bundle, not the backend.
14
14
  * - If only `eth_simulateV1` is configured, it gets the full timeout.
15
- *
16
- * `shareable` is Tenderly-only: when true, the backend persists the simulation and
17
- * returns a shareable URL. `eth_simulateV1` ignores it (no persistence concept).
18
15
  */
19
16
  export declare function executeSimulation(params: {
20
17
  config: SimulationConfig;
21
18
  chainId: number;
22
19
  transactions: SimulationTransaction[];
23
20
  blockNumber?: bigint | BlockTag;
24
- shareable: boolean;
25
21
  }): Promise<RawSimulationResult>;
@@ -1,5 +1,5 @@
1
1
  import { ExternalServiceError, UnsupportedChainError } from "../../errors.js";
2
- import { simulateTenderlyRest, simulateV1 } from "../backends/index.js";
2
+ import { simulateTenderlyRpc, simulateV1 } from "../backends/index.js";
3
3
  import { resolveChain } from "./resolve-chain.js";
4
4
  /** Total budget for a single `simulate()` call across both backends. */
5
5
  const DEFAULT_TIMEOUT_MS = 5000;
@@ -20,33 +20,27 @@ const FALLBACK_MIN_BUDGET_MS = 1500;
20
20
  *
21
21
  * Dispatches the transaction bundle to the available backend with a shared timeout
22
22
  * budget:
23
- * - If Tenderly is configured for the chain, it gets `TENDERLY_BUDGET_RATIO` (60%) of
24
- * the timeout. On `ExternalServiceError`, falls back to `eth_simulateV1` with
25
- * `max(remaining, FALLBACK_MIN_BUDGET_MS)` so the fallback still has a viable
26
- * window when Tenderly eats its full slice. `SimulationRevertedError` (contract
27
- * revert) propagates immediately without retry — a revert is a property of the
28
- * bundle, not the backend.
23
+ * - If a Tenderly RPC URL is configured for the chain, it gets
24
+ * `TENDERLY_BUDGET_RATIO` (60%) of the timeout. On `ExternalServiceError`,
25
+ * falls back to `eth_simulateV1` with `max(remaining, FALLBACK_MIN_BUDGET_MS)`
26
+ * so the fallback still has a viable window when Tenderly eats its full slice.
27
+ * `SimulationRevertedError` (contract revert) propagates immediately without
28
+ * retry — a revert is a property of the bundle, not the backend.
29
29
  * - If only `eth_simulateV1` is configured, it gets the full timeout.
30
- *
31
- * `shareable` is Tenderly-only: when true, the backend persists the simulation and
32
- * returns a shareable URL. `eth_simulateV1` ignores it (no persistence concept).
33
30
  */
34
31
  export async function executeSimulation(params) {
35
- const { config, chainId, transactions, blockNumber, shareable } = params;
32
+ const { config, chainId, transactions, blockNumber } = params;
36
33
  const chain = resolveChain(config, chainId);
37
34
  const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
38
35
  const deadline = Date.now() + timeoutMs;
39
- if (chain.tenderlySupported && config.tenderlyRest) {
36
+ if (chain.tenderlyRpc) {
40
37
  const tenderlyTimeout = Math.floor(timeoutMs * TENDERLY_BUDGET_RATIO);
41
38
  try {
42
- return await simulateTenderlyRest({
43
- config: config.tenderlyRest,
44
- chainId,
39
+ return await simulateTenderlyRpc({
40
+ config: chain.tenderlyRpc,
45
41
  transactions,
46
42
  blockNumber,
47
- shareable,
48
43
  signal: AbortSignal.timeout(tenderlyTimeout),
49
- logger: config.logger,
50
44
  });
51
45
  }
52
46
  catch (error) {
@@ -59,8 +53,6 @@ export async function executeSimulation(params) {
59
53
  });
60
54
  if (!chain.simulateV1Url)
61
55
  throw error;
62
- // Give the fallback a reasonable minimum even if Tenderly consumed the
63
- // whole budget — otherwise slow-Tenderly == total outage.
64
56
  const fallbackBudget = Math.max(deadline - Date.now(), FALLBACK_MIN_BUDGET_MS);
65
57
  return await simulateV1({
66
58
  rpcUrl: chain.simulateV1Url,
@@ -71,6 +63,7 @@ export async function executeSimulation(params) {
71
63
  });
72
64
  }
73
65
  }
66
+ /* v8 ignore next: resolveChain rejects this state before executeSimulation reaches the fallback path. */
74
67
  if (!chain.simulateV1Url) {
75
68
  throw new UnsupportedChainError(chainId);
76
69
  }
@@ -1,14 +1,15 @@
1
- import type { SimulationConfig } from "../../types.js";
1
+ import type { SimulationConfig, TenderlyRpcConfig } from "../../types.js";
2
2
  interface ChainCapability {
3
- tenderlySupported: boolean;
3
+ tenderlyRpc?: TenderlyRpcConfig;
4
4
  simulateV1Url?: string;
5
5
  }
6
6
  /**
7
7
  * Stage 2 of the simulate() pipeline.
8
8
  *
9
- * Determines which backend(s) are available for a chainId. Throws
10
- * `UnsupportedChainError` when neither Tenderly nor `eth_simulateV1` is reachable
11
- * for the requested chain.
9
+ * Looks up the per-chain `ChainSimulationConfig` and exposes whichever
10
+ * backends were configured. Throws `UnsupportedChainError` when the chain is
11
+ * absent from the map or has neither backend wired (defensive — the type
12
+ * already enforces at least one at construction).
12
13
  */
13
14
  export declare function resolveChain(config: SimulationConfig, chainId: number): ChainCapability;
14
15
  export {};
@@ -2,15 +2,18 @@ import { UnsupportedChainError } from "../../errors.js";
2
2
  /**
3
3
  * Stage 2 of the simulate() pipeline.
4
4
  *
5
- * Determines which backend(s) are available for a chainId. Throws
6
- * `UnsupportedChainError` when neither Tenderly nor `eth_simulateV1` is reachable
7
- * for the requested chain.
5
+ * Looks up the per-chain `ChainSimulationConfig` and exposes whichever
6
+ * backends were configured. Throws `UnsupportedChainError` when the chain is
7
+ * absent from the map or has neither backend wired (defensive — the type
8
+ * already enforces at least one at construction).
8
9
  */
9
10
  export function resolveChain(config, chainId) {
10
- const tenderlySupported = config.tenderlyRest?.supportedChainIds.has(chainId) ?? false;
11
- const simulateV1Url = config.chains.get(chainId)?.simulateV1Url;
12
- if (!tenderlySupported && !simulateV1Url) {
11
+ const entry = config.chains.get(chainId);
12
+ if (!entry || (!entry.tenderlyRpc && !entry.simulateV1Url)) {
13
13
  throw new UnsupportedChainError(chainId);
14
14
  }
15
- return { tenderlySupported, simulateV1Url };
15
+ return {
16
+ tenderlyRpc: entry.tenderlyRpc,
17
+ simulateV1Url: entry.simulateV1Url,
18
+ };
16
19
  }
@@ -3,20 +3,21 @@ import type { SimulateParams, SimulationConfig, SimulationResult } from "../type
3
3
  * Simulate a bundle of EVM transactions.
4
4
  *
5
5
  * Validates input → resolves authorizations into prepended approve txs → runs the bundle
6
- * through Tenderly REST (primary) or `eth_simulateV1` (fallback) with a shared timeout
6
+ * through Tenderly RPC (primary) or `eth_simulateV1` (fallback) with a shared timeout
7
7
  * budget → parses ERC20/WETH transfers from per-tx logs → asserts no funds are retained
8
8
  * by `bundler3` → returns the full result set. The caller reads whichever fields they need:
9
9
  *
10
- * - `transfers` + `tenderlyUrl` → user-facing preview.
11
- * - `simulationTxs` + `transfers` → server-side verification before broadcast, then pass
12
- * both to `screenAddresses` for compliance.
13
- * - `calls[i]` → per-tx raw backend output (`logs`, `status`, `returnData`, `gasUsed`,
14
- * and Tenderly-only `assetChanges`). Aligned 1:1 with `simulationTxs[i]`.
10
+ * - `transfers` → user-facing preview / server-side verification.
11
+ * - `simulationTxs` + `transfers` → server-side verification before broadcast.
12
+ * - `calls[i]` → per-tx raw backend output (`logs`, `status`, `returnData`, `gasUsed`).
13
+ * Aligned 1:1 with `simulationTxs[i]`.
14
+ * - `assetChanges` → net per-asset balance changes grouped by account (sender and
15
+ * counterparties) over the whole bundle, normalized to the same shape across backends.
15
16
  * - `transfers[k].txIdx` → index into `simulationTxs` of the tx that emitted the
16
17
  * underlying log; consumers map back via `simulationTxs[transfer.txIdx]`.
17
18
  *
18
- * @param config - Backend configuration: Tenderly credentials, per-chain RPC URLs, optional
19
- * logger, and the overall timeout budget.
19
+ * @param config - Backend configuration: per-chain Tenderly RPC and/or `eth_simulateV1`
20
+ * URL, optional logger, and the overall timeout budget.
20
21
  * @param params - Per-call simulation input.
21
22
  * @param params.chainId - Chain id the bundle targets.
22
23
  * @param params.transactions - The bundle's transactions, in execution order. All must share the
@@ -24,10 +25,6 @@ import type { SimulateParams, SimulationConfig, SimulationResult } from "../type
24
25
  * @param params.authorizations - Optional token authorizations resolved into prepended approve
25
26
  * transactions before the main bundle runs.
26
27
  * @param params.blockNumber - Optional pinned block number or `BlockTag`. Defaults to `latest`.
27
- * @param options - Per-call simulation options.
28
- * @param options.shareable - When `true`, persists the simulation in Tenderly and returns a
29
- * shareable `tenderlyUrl`. Honored only on the Tenderly backend, not on the `eth_simulateV1`
30
- * fallback. Defaults to `false`.
31
28
  * @throws {SimulationValidationError} for invalid input (mixed senders, bad addresses,
32
29
  * empty transactions, malformed authorizations).
33
30
  * @throws {UnsupportedChainError} when the chain is not configured for any backend.
@@ -39,14 +36,21 @@ import type { SimulateParams, SimulationConfig, SimulationResult } from "../type
39
36
  * not match the resolved `simulationTxs` — refusing to map transfers with mismatched
40
37
  * per-tx output.
41
38
  * @returns A {@link SimulationResult} carrying the resolved `simulationTxs`, per-tx
42
- * `calls` (aligned 1:1 with `simulationTxs`), parsed `transfers` (each stamped with
43
- * `txIdx`), and the optional `tenderlyUrl`.
39
+ * `calls` (aligned 1:1 with `simulationTxs`), parsed `transfers` (each stamped
40
+ * with `txIdx`), and per-account net `assetChanges`.
44
41
  * @example
45
42
  * ```ts
46
43
  * import { simulate } from "@morpho-org/evm-simulation";
47
44
  *
48
45
  * const result = await simulate(
49
- * { chains: new Map([[1, { simulateV1Url: rpcUrl }]]) },
46
+ * {
47
+ * chains: new Map([
48
+ * [1, {
49
+ * tenderlyRpc: { rpcUrl: process.env.TENDERLY_RPC_URL! },
50
+ * simulateV1Url: process.env.MAINNET_RPC_URL,
51
+ * }],
52
+ * ]),
53
+ * },
50
54
  * {
51
55
  * chainId: 1,
52
56
  * transactions: [{ from: user, to: vaultAddress, data: encodedCalldata, value: 0n }],
@@ -55,11 +59,4 @@ import type { SimulateParams, SimulationConfig, SimulationResult } from "../type
55
59
  * // result satisfies SimulationResult
56
60
  * ```
57
61
  */
58
- export declare function simulate(config: SimulationConfig, params: SimulateParams, options?: {
59
- /**
60
- * When true, persist the simulation in Tenderly and return a shareable
61
- * `tenderlyUrl` in the result. Only honored when the Tenderly backend runs
62
- * (not the `eth_simulateV1` fallback). Defaults to `false`.
63
- */
64
- shareable?: boolean;
65
- }): Promise<SimulationResult>;
62
+ export declare function simulate(config: SimulationConfig, params: SimulateParams): Promise<SimulationResult>;
@@ -5,20 +5,21 @@ import { assertNoBundlerRetention, buildSimulationTxs, executeSimulation, valida
5
5
  * Simulate a bundle of EVM transactions.
6
6
  *
7
7
  * Validates input → resolves authorizations into prepended approve txs → runs the bundle
8
- * through Tenderly REST (primary) or `eth_simulateV1` (fallback) with a shared timeout
8
+ * through Tenderly RPC (primary) or `eth_simulateV1` (fallback) with a shared timeout
9
9
  * budget → parses ERC20/WETH transfers from per-tx logs → asserts no funds are retained
10
10
  * by `bundler3` → returns the full result set. The caller reads whichever fields they need:
11
11
  *
12
- * - `transfers` + `tenderlyUrl` → user-facing preview.
13
- * - `simulationTxs` + `transfers` → server-side verification before broadcast, then pass
14
- * both to `screenAddresses` for compliance.
15
- * - `calls[i]` → per-tx raw backend output (`logs`, `status`, `returnData`, `gasUsed`,
16
- * and Tenderly-only `assetChanges`). Aligned 1:1 with `simulationTxs[i]`.
12
+ * - `transfers` → user-facing preview / server-side verification.
13
+ * - `simulationTxs` + `transfers` → server-side verification before broadcast.
14
+ * - `calls[i]` → per-tx raw backend output (`logs`, `status`, `returnData`, `gasUsed`).
15
+ * Aligned 1:1 with `simulationTxs[i]`.
16
+ * - `assetChanges` → net per-asset balance changes grouped by account (sender and
17
+ * counterparties) over the whole bundle, normalized to the same shape across backends.
17
18
  * - `transfers[k].txIdx` → index into `simulationTxs` of the tx that emitted the
18
19
  * underlying log; consumers map back via `simulationTxs[transfer.txIdx]`.
19
20
  *
20
- * @param config - Backend configuration: Tenderly credentials, per-chain RPC URLs, optional
21
- * logger, and the overall timeout budget.
21
+ * @param config - Backend configuration: per-chain Tenderly RPC and/or `eth_simulateV1`
22
+ * URL, optional logger, and the overall timeout budget.
22
23
  * @param params - Per-call simulation input.
23
24
  * @param params.chainId - Chain id the bundle targets.
24
25
  * @param params.transactions - The bundle's transactions, in execution order. All must share the
@@ -26,10 +27,6 @@ import { assertNoBundlerRetention, buildSimulationTxs, executeSimulation, valida
26
27
  * @param params.authorizations - Optional token authorizations resolved into prepended approve
27
28
  * transactions before the main bundle runs.
28
29
  * @param params.blockNumber - Optional pinned block number or `BlockTag`. Defaults to `latest`.
29
- * @param options - Per-call simulation options.
30
- * @param options.shareable - When `true`, persists the simulation in Tenderly and returns a
31
- * shareable `tenderlyUrl`. Honored only on the Tenderly backend, not on the `eth_simulateV1`
32
- * fallback. Defaults to `false`.
33
30
  * @throws {SimulationValidationError} for invalid input (mixed senders, bad addresses,
34
31
  * empty transactions, malformed authorizations).
35
32
  * @throws {UnsupportedChainError} when the chain is not configured for any backend.
@@ -41,14 +38,21 @@ import { assertNoBundlerRetention, buildSimulationTxs, executeSimulation, valida
41
38
  * not match the resolved `simulationTxs` — refusing to map transfers with mismatched
42
39
  * per-tx output.
43
40
  * @returns A {@link SimulationResult} carrying the resolved `simulationTxs`, per-tx
44
- * `calls` (aligned 1:1 with `simulationTxs`), parsed `transfers` (each stamped with
45
- * `txIdx`), and the optional `tenderlyUrl`.
41
+ * `calls` (aligned 1:1 with `simulationTxs`), parsed `transfers` (each stamped
42
+ * with `txIdx`), and per-account net `assetChanges`.
46
43
  * @example
47
44
  * ```ts
48
45
  * import { simulate } from "@morpho-org/evm-simulation";
49
46
  *
50
47
  * const result = await simulate(
51
- * { chains: new Map([[1, { simulateV1Url: rpcUrl }]]) },
48
+ * {
49
+ * chains: new Map([
50
+ * [1, {
51
+ * tenderlyRpc: { rpcUrl: process.env.TENDERLY_RPC_URL! },
52
+ * simulateV1Url: process.env.MAINNET_RPC_URL,
53
+ * }],
54
+ * ]),
55
+ * },
52
56
  * {
53
57
  * chainId: 1,
54
58
  * transactions: [{ from: user, to: vaultAddress, data: encodedCalldata, value: 0n }],
@@ -57,9 +61,7 @@ import { assertNoBundlerRetention, buildSimulationTxs, executeSimulation, valida
57
61
  * // result satisfies SimulationResult
58
62
  * ```
59
63
  */
60
- // biome-ignore lint/complexity/useMaxParams: TODO refactor to ≤2 params
61
- export async function simulate(config, params, options = {}) {
62
- const { shareable = false } = options;
64
+ export async function simulate(config, params) {
63
65
  validateInput(params);
64
66
  const simulationTxs = buildSimulationTxs(params);
65
67
  const result = await executeSimulation({
@@ -67,7 +69,6 @@ export async function simulate(config, params, options = {}) {
67
69
  chainId: params.chainId,
68
70
  transactions: simulationTxs,
69
71
  blockNumber: params.blockNumber,
70
- shareable,
71
72
  });
72
73
  if (result.calls.length !== simulationTxs.length) {
73
74
  throw new ExternalServiceError(`Backend returned ${result.calls.length} call result(s) for ${simulationTxs.length} transaction(s) — refusing to map transfers with mismatched lengths`);
@@ -82,6 +83,6 @@ export async function simulate(config, params, options = {}) {
82
83
  simulationTxs,
83
84
  calls: result.calls,
84
85
  transfers,
85
- tenderlyUrl: result.tenderlyUrl,
86
+ assetChanges: result.assetChanges,
86
87
  };
87
88
  }