@morpho-org/evm-simulation 1.0.0 → 2.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.
package/README.md CHANGED
@@ -1,9 +1,11 @@
1
1
  # @morpho-org/evm-simulation
2
2
 
3
+ ## Overview
4
+
3
5
  EVM transaction simulation engine for Morpho — bundle execution preview,
4
6
  transfer parsing, and compliance screening.
5
7
 
6
- ## Install
8
+ ## Installation
7
9
 
8
10
  Add to the consuming app's `package.json`:
9
11
 
@@ -15,7 +17,7 @@ Add to the consuming app's `package.json`:
15
17
  }
16
18
  ```
17
19
 
18
- ## Quick start
20
+ ## Usage
19
21
 
20
22
  ```ts
21
23
  import {
@@ -60,7 +62,7 @@ try {
60
62
  }
61
63
  ```
62
64
 
63
- ## API surface
65
+ ### API surface
64
66
 
65
67
  All symbols below are re-exported from the package root.
66
68
 
@@ -70,8 +72,16 @@ All symbols below are re-exported from the package root.
70
72
  - Data types: `SimulationTransaction`, `SimulationAuthorization`, `Transfer`, `SimulateParams`, `SimulationResult`.
71
73
  - Errors: `SimulationPackageError` (abstract base — `instanceof` it to catch any package error), `SimulationRevertedError`, `BlacklistViolationError`, `AddressScreeningError`, `ExternalServiceError`, `SimulationValidationError`, `UnsupportedChainError`.
72
74
 
73
- ## Deeper docs
75
+ ### Deeper docs
74
76
 
75
77
  See [`CLAUDE.md`](./CLAUDE.md) in this directory for the execution flow diagram,
76
78
  backend tradeoffs, authorizations model, screening design, error-handling table,
77
79
  and recipes for adding a chain or a new backend.
80
+
81
+ ## Development
82
+
83
+ Contribute from the monorepo root. See [CONTRIBUTING.md](../../CONTRIBUTING.md) for setup, checks, and package workflow. Report vulnerabilities through [SECURITY.md](../../SECURITY.md).
84
+
85
+ ## License
86
+
87
+ MIT. See [LICENSE](./LICENSE).
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Base error for all simulation package errors.
3
- * Transport-agnostic — no HTTP status codes.
4
- * Consumers use try/catch + instanceof to handle specific error types.
2
+ * Base class for every error this package throws. Transport-agnostic — no HTTP status codes.
3
+ * Consumers pattern-match with `instanceof` on the concrete subclass.
5
4
  */
6
5
  export declare abstract class SimulationPackageError extends Error {
6
+ /** Stable string discriminator for log aggregation and external mapping. */
7
7
  abstract readonly code: string;
8
8
  constructor(message: string, options?: ErrorOptions);
9
9
  }
package/lib/cjs/errors.js CHANGED
@@ -2,9 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.UnsupportedChainError = exports.SimulationValidationError = exports.ExternalServiceError = exports.AddressScreeningError = exports.BlacklistViolationError = exports.SimulationRevertedError = exports.SimulationPackageError = void 0;
4
4
  /**
5
- * Base error for all simulation package errors.
6
- * Transport-agnostic — no HTTP status codes.
7
- * Consumers use try/catch + instanceof to handle specific error types.
5
+ * Base class for every error this package throws. Transport-agnostic — no HTTP status codes.
6
+ * Consumers pattern-match with `instanceof` on the concrete subclass.
8
7
  */
9
8
  class SimulationPackageError extends Error {
10
9
  constructor(message, options) {
@@ -1,4 +1,4 @@
1
+ export { AddressScreeningError, BlacklistViolationError, ExternalServiceError, SimulationPackageError, SimulationRevertedError, SimulationValidationError, UnsupportedChainError, } from "./errors.js";
1
2
  export { screenAddresses } from "./screen-addresses/index.js";
2
3
  export { simulate } from "./simulate/index.js";
3
- export type { ChainSimulationConfig, SimulateParams, SimulationAuthorization, SimulationConfig, SimulationLogger, SimulationResult, SimulationTransaction, TenderlyRestConfig, Transfer, } from "./types.js";
4
- export { SimulationPackageError, SimulationRevertedError, BlacklistViolationError, AddressScreeningError, ExternalServiceError, SimulationValidationError, UnsupportedChainError, } from "./errors.js";
4
+ export type { ChainSimulationConfig, RawLog, SimulateParams, SimulationAuthorization, SimulationCall, SimulationConfig, SimulationLogger, SimulationResult, SimulationTransaction, TenderlyRestConfig, Transfer, } from "./types.js";
package/lib/cjs/index.js CHANGED
@@ -1,17 +1,17 @@
1
1
  "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.UnsupportedChainError = exports.SimulationValidationError = exports.ExternalServiceError = exports.AddressScreeningError = exports.BlacklistViolationError = exports.SimulationRevertedError = exports.SimulationPackageError = exports.simulate = exports.screenAddresses = void 0;
4
2
  // Public fns (via feature-folder barrels)
5
- var index_js_1 = require("./screen-addresses/index.js");
6
- Object.defineProperty(exports, "screenAddresses", { enumerable: true, get: function () { return index_js_1.screenAddresses; } });
7
- var index_js_2 = require("./simulate/index.js");
8
- Object.defineProperty(exports, "simulate", { enumerable: true, get: function () { return index_js_2.simulate; } });
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.simulate = exports.screenAddresses = exports.UnsupportedChainError = exports.SimulationValidationError = exports.SimulationRevertedError = exports.SimulationPackageError = exports.ExternalServiceError = exports.BlacklistViolationError = exports.AddressScreeningError = void 0;
9
5
  // Errors (for instanceof checks by consumers)
10
6
  var errors_js_1 = require("./errors.js");
11
- Object.defineProperty(exports, "SimulationPackageError", { enumerable: true, get: function () { return errors_js_1.SimulationPackageError; } });
12
- Object.defineProperty(exports, "SimulationRevertedError", { enumerable: true, get: function () { return errors_js_1.SimulationRevertedError; } });
13
- Object.defineProperty(exports, "BlacklistViolationError", { enumerable: true, get: function () { return errors_js_1.BlacklistViolationError; } });
14
7
  Object.defineProperty(exports, "AddressScreeningError", { enumerable: true, get: function () { return errors_js_1.AddressScreeningError; } });
8
+ Object.defineProperty(exports, "BlacklistViolationError", { enumerable: true, get: function () { return errors_js_1.BlacklistViolationError; } });
15
9
  Object.defineProperty(exports, "ExternalServiceError", { enumerable: true, get: function () { return errors_js_1.ExternalServiceError; } });
10
+ Object.defineProperty(exports, "SimulationPackageError", { enumerable: true, get: function () { return errors_js_1.SimulationPackageError; } });
11
+ Object.defineProperty(exports, "SimulationRevertedError", { enumerable: true, get: function () { return errors_js_1.SimulationRevertedError; } });
16
12
  Object.defineProperty(exports, "SimulationValidationError", { enumerable: true, get: function () { return errors_js_1.SimulationValidationError; } });
17
13
  Object.defineProperty(exports, "UnsupportedChainError", { enumerable: true, get: function () { return errors_js_1.UnsupportedChainError; } });
14
+ var index_js_1 = require("./screen-addresses/index.js");
15
+ Object.defineProperty(exports, "screenAddresses", { enumerable: true, get: function () { return index_js_1.screenAddresses; } });
16
+ var index_js_2 = require("./simulate/index.js");
17
+ Object.defineProperty(exports, "simulate", { enumerable: true, get: function () { return index_js_2.simulate; } });
@@ -1,29 +1,45 @@
1
1
  import type { SimulationLogger, SimulationTransaction, Transfer } from "../types.js";
2
2
  /**
3
- * Screen all relevant addresses from a simulation for sanctioned/compromised addresses.
3
+ * Screens every address that appears in a simulated bundle (transfer senders and recipients,
4
+ * plus tx `from` / `to`) against the static {@link sanctionedAddresses} list and, when
5
+ * configured, the Chainalysis Entity API.
4
6
  *
5
- * Screening targets (deduped, lowercased, zero-addr removed):
6
- * - Transfer senders and recipients from simulation result
7
- * - `from` and `to` of every tx in `simulationTxs` (catches native-value flows)
7
+ * Addresses are deduped and lowercased; the zero address is removed. Checks run in order:
8
8
  *
9
- * Checks run in order:
10
- * 1. Static {@link sanctionedAddresses} set (fast, no network, always on).
11
- * 2. Chainalysis Entity API — one lookup per address, lookups across addresses
12
- * run in parallel. Only fires if `chainalysisApiKey` is provided.
13
- * **Fail-open on transport errors before registration** (network / timeout /
14
- * non-ok POST): the address is treated as clean, a warn is logged, and the
15
- * simulation proceeds. This is the documented posture — a Chainalysis
16
- * outage must not block all simulations.
17
- * **Fail-closed on partial registration**: if the registration POST succeeded
18
- * but the subsequent lookup GET fails, the address is treated as Severe.
19
- * The data already persisted server-side, so failing open at this point
20
- * would let a pass slip through based on a half-completed check.
21
- * **Fail-closed on schema drift**: if Chainalysis returns an unknown risk
22
- * tier or a body that doesn't parse, the address is treated as Severe so a
23
- * future stricter tier isn't silently downgraded.
9
+ * 1. **Static sanctioned list** — always on, no network. Throws on the first hit.
10
+ * 2. **Chainalysis Entity API** — one lookup per address, lookups parallelized. Only runs when
11
+ * `chainalysisApiKey` is provided.
12
+ * - **Fail-open on transport errors before registration** (network / timeout / non-ok POST):
13
+ * treat the address as clean and log a warn. A Chainalysis outage must not block all
14
+ * simulations.
15
+ * - **Fail-closed on partial registration**: if the registration POST succeeded but the
16
+ * lookup GET fails, treat the address as Severe.
17
+ * - **Fail-closed on schema drift**: an unknown risk tier or unparsed body is treated as
18
+ * Severe so a future stricter tier is not silently downgraded.
24
19
  *
25
- * Throws `AddressScreeningError` on any Severe hit. Intended to be called after
26
- * `simulate`, passing the returned `simulationTxs` and `transfers`.
20
+ * Intended to be called after {@link simulate}, passing the returned `simulationTxs` and
21
+ * `transfers`.
22
+ *
23
+ * @param params.simulationTxs - The resolved transactions returned by `simulate()`.
24
+ * @param params.transfers - The parsed transfers returned by `simulate()`.
25
+ * @param params.chainalysisApiKey - Optional Chainalysis token. When absent, only the static
26
+ * sanctioned list runs.
27
+ * @param params.signal - Optional `AbortSignal` propagated to every Chainalysis request.
28
+ * @param params.logger - Optional `SimulationLogger` for warn-level diagnostics.
29
+ * @returns Resolves to `void` on a clean screening; throws on any Severe hit.
30
+ * @throws {AddressScreeningError} when any address matches the static sanctioned list or
31
+ * resolves to Severe via Chainalysis.
32
+ * @example
33
+ * ```ts
34
+ * import { screenAddresses, simulate } from "@morpho-org/evm-simulation";
35
+ *
36
+ * const result = await simulate(config, params);
37
+ * await screenAddresses({
38
+ * simulationTxs: result.simulationTxs,
39
+ * transfers: result.transfers,
40
+ * chainalysisApiKey: process.env.CHAINALYSIS_API_KEY,
41
+ * });
42
+ * ```
27
43
  */
28
44
  export declare function screenAddresses(params: {
29
45
  simulationTxs: SimulationTransaction[];
@@ -19,30 +19,46 @@ const chainalysisEntitySchema = zod_1.z
19
19
  })
20
20
  .passthrough();
21
21
  /**
22
- * Screen all relevant addresses from a simulation for sanctioned/compromised addresses.
22
+ * Screens every address that appears in a simulated bundle (transfer senders and recipients,
23
+ * plus tx `from` / `to`) against the static {@link sanctionedAddresses} list and, when
24
+ * configured, the Chainalysis Entity API.
23
25
  *
24
- * Screening targets (deduped, lowercased, zero-addr removed):
25
- * - Transfer senders and recipients from simulation result
26
- * - `from` and `to` of every tx in `simulationTxs` (catches native-value flows)
26
+ * Addresses are deduped and lowercased; the zero address is removed. Checks run in order:
27
27
  *
28
- * Checks run in order:
29
- * 1. Static {@link sanctionedAddresses} set (fast, no network, always on).
30
- * 2. Chainalysis Entity API — one lookup per address, lookups across addresses
31
- * run in parallel. Only fires if `chainalysisApiKey` is provided.
32
- * **Fail-open on transport errors before registration** (network / timeout /
33
- * non-ok POST): the address is treated as clean, a warn is logged, and the
34
- * simulation proceeds. This is the documented posture — a Chainalysis
35
- * outage must not block all simulations.
36
- * **Fail-closed on partial registration**: if the registration POST succeeded
37
- * but the subsequent lookup GET fails, the address is treated as Severe.
38
- * The data already persisted server-side, so failing open at this point
39
- * would let a pass slip through based on a half-completed check.
40
- * **Fail-closed on schema drift**: if Chainalysis returns an unknown risk
41
- * tier or a body that doesn't parse, the address is treated as Severe so a
42
- * future stricter tier isn't silently downgraded.
28
+ * 1. **Static sanctioned list** — always on, no network. Throws on the first hit.
29
+ * 2. **Chainalysis Entity API** — one lookup per address, lookups parallelized. Only runs when
30
+ * `chainalysisApiKey` is provided.
31
+ * - **Fail-open on transport errors before registration** (network / timeout / non-ok POST):
32
+ * treat the address as clean and log a warn. A Chainalysis outage must not block all
33
+ * simulations.
34
+ * - **Fail-closed on partial registration**: if the registration POST succeeded but the
35
+ * lookup GET fails, treat the address as Severe.
36
+ * - **Fail-closed on schema drift**: an unknown risk tier or unparsed body is treated as
37
+ * Severe so a future stricter tier is not silently downgraded.
43
38
  *
44
- * Throws `AddressScreeningError` on any Severe hit. Intended to be called after
45
- * `simulate`, passing the returned `simulationTxs` and `transfers`.
39
+ * Intended to be called after {@link simulate}, passing the returned `simulationTxs` and
40
+ * `transfers`.
41
+ *
42
+ * @param params.simulationTxs - The resolved transactions returned by `simulate()`.
43
+ * @param params.transfers - The parsed transfers returned by `simulate()`.
44
+ * @param params.chainalysisApiKey - Optional Chainalysis token. When absent, only the static
45
+ * sanctioned list runs.
46
+ * @param params.signal - Optional `AbortSignal` propagated to every Chainalysis request.
47
+ * @param params.logger - Optional `SimulationLogger` for warn-level diagnostics.
48
+ * @returns Resolves to `void` on a clean screening; throws on any Severe hit.
49
+ * @throws {AddressScreeningError} when any address matches the static sanctioned list or
50
+ * resolves to Severe via Chainalysis.
51
+ * @example
52
+ * ```ts
53
+ * import { screenAddresses, simulate } from "@morpho-org/evm-simulation";
54
+ *
55
+ * const result = await simulate(config, params);
56
+ * await screenAddresses({
57
+ * simulationTxs: result.simulationTxs,
58
+ * transfers: result.transfers,
59
+ * chainalysisApiKey: process.env.CHAINALYSIS_API_KEY,
60
+ * });
61
+ * ```
46
62
  */
47
63
  async function screenAddresses(params) {
48
64
  const { simulationTxs, transfers, chainalysisApiKey, signal, logger } = params;
@@ -103,7 +119,10 @@ async function screenAddresses(params) {
103
119
  *
104
120
  * A SINGLE deadline covers both the POST and the GET, keeping the per-address
105
121
  * wall-time budget at `CHAINALYSIS_TIMEOUT_MS` (5 s) rather than doubling it.
122
+ *
123
+ * @internal
106
124
  */
125
+ // biome-ignore lint/complexity/useMaxParams: TODO refactor to ≤2 params
107
126
  async function screenWithChainalysis(address, apiKey, parentSignal) {
108
127
  const headers = {
109
128
  "Content-Type": "application/json",
@@ -54,17 +54,17 @@ async function simulateV1(params) {
54
54
  if (failedResult) {
55
55
  throw new errors_js_1.SimulationRevertedError(failedResult.error?.message ?? "Simulation failed", results);
56
56
  }
57
- const rawLogs = [];
58
- for (const r of results) {
59
- for (const log of r.logs ?? []) {
60
- rawLogs.push({
61
- address: log.address,
62
- topics: [...log.topics],
63
- data: log.data ?? "0x",
64
- });
65
- }
66
- }
67
- return { logs: rawLogs };
57
+ const rawCalls = results.map((r) => ({
58
+ logs: (r.logs ?? []).map((log) => ({
59
+ address: log.address,
60
+ topics: [...log.topics],
61
+ data: log.data ?? "0x",
62
+ })),
63
+ status: r.status === "success",
64
+ returnData: r.data ?? "0x",
65
+ gasUsed: r.gasUsed,
66
+ }));
67
+ return { calls: rawCalls };
68
68
  }
69
69
  catch (error) {
70
70
  if (error instanceof errors_js_1.SimulationRevertedError)
@@ -22,6 +22,7 @@ const tenderlyRawResponseSchema = zod_1.z
22
22
  transaction: zod_1.z
23
23
  .object({
24
24
  error_message: zod_1.z.string().optional(),
25
+ gas_used: zod_1.z.number(),
25
26
  transaction_info: zod_1.z
26
27
  .object({
27
28
  logs: zod_1.z
@@ -39,6 +40,11 @@ const tenderlyRawResponseSchema = zod_1.z
39
40
  .passthrough())
40
41
  .optional(),
41
42
  asset_changes: zod_1.z.unknown().optional(),
43
+ call_trace: zod_1.z
44
+ .object({
45
+ output: hexSchema,
46
+ })
47
+ .passthrough(),
42
48
  })
43
49
  .passthrough(),
44
50
  })
@@ -101,6 +107,7 @@ function buildBaseUrl(config) {
101
107
  return `${config.apiBaseUrl}/api/v1/account/${account}/project/${project}`;
102
108
  }
103
109
  /** Build the `TenderlySimulateRequest` body for one transaction. Shared by single + bundle. */
110
+ // biome-ignore lint/complexity/useMaxParams: TODO refactor to ≤2 params
104
111
  function buildTxBody(tx, chainId, shareable, blockNumber) {
105
112
  return {
106
113
  network_id: String(chainId),
@@ -132,19 +139,16 @@ async function simulateSingle(params) {
132
139
  throw new errors_js_1.ExternalServiceError(`Tenderly REST API returned ${response.status}: ${response.statusText}`);
133
140
  }
134
141
  const data = tenderlyRawResponseSchema.parse(await response.json());
135
- const result = parseTenderlyResponse(data, shareable);
136
- if (shareable && data.simulation.id) {
137
- const shared = await shareSimulation({
138
- baseUrl,
139
- config,
140
- simulationId: data.simulation.id,
141
- signal,
142
- logger,
143
- });
144
- if (!shared)
145
- result.tenderlyUrl = undefined;
146
- }
147
- return result;
142
+ const call = parseTenderlyCall(data);
143
+ const tenderlyUrl = await resolveTenderlyUrl({
144
+ baseUrl,
145
+ config,
146
+ simulationId: data.simulation.id,
147
+ shareable,
148
+ signal,
149
+ logger,
150
+ });
151
+ return { calls: [call], tenderlyUrl };
148
152
  }
149
153
  async function simulateBundle(params) {
150
154
  const { baseUrl, config, chainId, transactions, blockNumber, shareable, signal, logger, } = params;
@@ -165,39 +169,40 @@ async function simulateBundle(params) {
165
169
  }
166
170
  const data = tenderlyBundleRawResponseSchema.parse(await response.json());
167
171
  const simulations = data.simulation_results;
168
- // Collect logs from ALL steps (approvals + main tx) for complete bundler retention coverage.
169
- // Only build the shareable URL from the last step; others parse with shareable=false.
170
- const allLogs = [];
171
- let lastResult;
172
- let lastSimulationId;
173
- for (let i = 0; i < simulations.length; i++) {
174
- const sim = simulations[i];
175
- const isLast = i === simulations.length - 1;
176
- const parsed = parseTenderlyResponse(sim, isLast && shareable);
177
- allLogs.push(...parsed.logs);
178
- if (isLast) {
179
- lastResult = parsed;
180
- lastSimulationId = sim.simulation.id;
181
- }
182
- }
183
- // Schema enforces `.min(1)` — lastResult is always defined after the loop.
184
- let tenderlyUrl = lastResult.tenderlyUrl;
185
- if (shareable && tenderlyUrl && lastSimulationId) {
186
- const shared = await shareSimulation({
187
- baseUrl,
188
- config,
189
- simulationId: lastSimulationId,
190
- signal,
191
- logger,
192
- });
193
- if (!shared)
194
- tenderlyUrl = undefined;
195
- }
196
- return {
197
- logs: allLogs,
198
- tenderlyUrl,
199
- rawAssetChanges: lastResult.rawAssetChanges,
200
- };
172
+ const calls = simulations.map((sim) => parseTenderlyCall(sim));
173
+ const lastSim = simulations[simulations.length - 1];
174
+ const tenderlyUrl = await resolveTenderlyUrl({
175
+ baseUrl,
176
+ config,
177
+ simulationId: lastSim.simulation.id,
178
+ shareable,
179
+ signal,
180
+ logger,
181
+ });
182
+ return { calls, tenderlyUrl };
183
+ }
184
+ /**
185
+ * Build the shareable Tenderly dashboard URL for the given simulation, if
186
+ * `shareable=true` and `/share` succeeds. Returns `undefined` otherwise.
187
+ *
188
+ * Mirrors the legacy "build optimistically, clear on failure" pattern: the
189
+ * URL is constructed up-front and cleared if `shareSimulation` returns false.
190
+ */
191
+ async function resolveTenderlyUrl(params) {
192
+ const { baseUrl, config, simulationId, shareable, signal, logger } = params;
193
+ if (!shareable || !simulationId)
194
+ return undefined;
195
+ let tenderlyUrl = `https://dashboard.tenderly.co/shared/simulation/${simulationId}`;
196
+ const shared = await shareSimulation({
197
+ baseUrl,
198
+ config,
199
+ simulationId,
200
+ signal,
201
+ logger,
202
+ });
203
+ if (!shared)
204
+ tenderlyUrl = undefined;
205
+ return tenderlyUrl;
201
206
  }
202
207
  /**
203
208
  * Try to make a simulation publicly shareable. Returns true iff Tenderly's
@@ -236,7 +241,13 @@ async function shareSimulation(params) {
236
241
  return false;
237
242
  }
238
243
  }
239
- function parseTenderlyResponse(data, shareable) {
244
+ /**
245
+ * Parse one Tenderly simulation response into a `RawCall`. The caller
246
+ * (single-sim or bundle) assembles the `RawSimulationResult` around it.
247
+ *
248
+ * Throws `SimulationRevertedError` if `simulation.status !== true`.
249
+ */
250
+ function parseTenderlyCall(data) {
240
251
  const { simulation, transaction } = data;
241
252
  if (simulation.status !== true) {
242
253
  const errorMessage = transaction.error_message ||
@@ -258,12 +269,11 @@ function parseTenderlyResponse(data, shareable) {
258
269
  });
259
270
  }
260
271
  }
261
- const tenderlyUrl = shareable && simulation.id
262
- ? `https://dashboard.tenderly.co/shared/simulation/${simulation.id}`
263
- : undefined;
264
272
  return {
265
273
  logs: rawLogs,
266
- tenderlyUrl,
267
- rawAssetChanges: transaction.transaction_info.asset_changes,
274
+ status: true,
275
+ returnData: transaction.transaction_info.call_trace.output,
276
+ gasUsed: BigInt(transaction.gas_used),
277
+ assetChanges: transaction.transaction_info.asset_changes,
268
278
  };
269
279
  }
@@ -1,27 +1,39 @@
1
- import type { RawLog, SimulationLogger, Transfer } from "../../types.js";
1
+ import type { RawCall, SimulationLogger, Transfer } from "../../types.js";
2
2
  export declare const TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
3
3
  export declare const WITHDRAWAL_TOPIC = "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65";
4
4
  export declare const DEPOSIT_TOPIC = "0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c";
5
5
  /**
6
6
  * Parse raw EVM logs into individual Transfer events.
7
7
  *
8
+ * Iterates over per-tx `calls` and stamps each emitted `Transfer.txIdx`
9
+ * with the index of the originating call.
10
+ *
8
11
  * **Supported event types:** ERC20 `Transfer(from, to, amount)` and WETH9
9
12
  * `Deposit(to, amount)` / `Withdrawal(from, amount)`. WETH9 mint/burn Transfer
10
13
  * events paired with their Deposit/Withdrawal are deduplicated. ERC721 and
11
14
  * ERC1155 transfer events are **not** parsed — consumers with NFT flows will
12
15
  * see an incomplete transfer list.
13
16
  *
14
- * **WETH9-specific dedup:** matches Transfer-to/from zero against the
15
- * matching Deposit/Withdrawal event on the same contract with the same data
16
- * and withdrawer. Non-standard wrapped-native implementations (e.g., rebasing
17
- * or fee-on-withdraw wrappers) may escape the dedup and produce double-counts.
17
+ * **WETH9 dedup assumption — canonical atomic emission.** Dedup is scoped to
18
+ * the same tx: a zero-address `Transfer` is suppressed only if its paired
19
+ * `Deposit`/`Withdrawal` appears in the same `calls[txIdx].logs` slice. This
20
+ * is correct for canonical WETH9, which always emits the `Deposit`/
21
+ * `Withdrawal` and the matching `Transfer(0x0, …)` / `Transfer(…, 0x0)`
22
+ * atomically inside a single call frame. A **non-canonical wrapped-native**
23
+ * that splits these emissions across two txs in the same bundle would leave
24
+ * a phantom zero-address `Transfer` in the parsed output, which can be summed
25
+ * by `assertNoBundlerRetention` and produce a false `BlacklistViolationError`.
26
+ * When a zero-address `Transfer` misses same-tx dedup *and* its token has
27
+ * emitted a `Deposit`/`Withdrawal` somewhere else in the bundle (i.e. it
28
+ * looks wnative-shaped), the parser emits a `warn` so the assumption break
29
+ * is observable before it reaches retention. None of `morpho-sdk`'s currently
30
+ * supported chains require cross-tx dedup; the assumption is rechecked when
31
+ * onboarding a new chain (see `evm-simulation/CLAUDE.md`).
18
32
  *
19
33
  * **Failure mode:** on a per-log parse failure (malformed topic length,
20
34
  * non-hex data), the log is skipped and a `warn` is emitted via the logger.
21
- * The caller receives a possibly-incomplete transfer list. Upstream sources
22
- * (e.g., Tenderly response schemas) should validate log shape before calling
23
- * here to avoid silent drops.
24
35
  *
25
36
  * Output is sorted canonically by token, from, to, amount for determinism.
37
+ * `txIdx` is attached but does not influence sort order.
26
38
  */
27
- export declare function parseTransfers(logs: RawLog[], logger?: SimulationLogger): Transfer[];
39
+ export declare function parseTransfers(calls: readonly RawCall[], logger?: SimulationLogger): Transfer[];