@morpho-org/evm-simulation 1.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 (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -0
  3. package/lib/cjs/errors.d.ts +50 -0
  4. package/lib/cjs/errors.js +72 -0
  5. package/lib/cjs/index.d.ts +4 -0
  6. package/lib/cjs/index.js +17 -0
  7. package/lib/cjs/package.json +1 -0
  8. package/lib/cjs/screen-addresses/index.d.ts +1 -0
  9. package/lib/cjs/screen-addresses/index.js +5 -0
  10. package/lib/cjs/screen-addresses/sanctioned-addresses.d.ts +11 -0
  11. package/lib/cjs/screen-addresses/sanctioned-addresses.js +77 -0
  12. package/lib/cjs/screen-addresses/screen-addresses.d.ts +34 -0
  13. package/lib/cjs/screen-addresses/screen-addresses.js +152 -0
  14. package/lib/cjs/simulate/authorizations/index.d.ts +2 -0
  15. package/lib/cjs/simulate/authorizations/index.js +7 -0
  16. package/lib/cjs/simulate/authorizations/resolve.d.ts +9 -0
  17. package/lib/cjs/simulate/authorizations/resolve.js +29 -0
  18. package/lib/cjs/simulate/authorizations/validate.d.ts +6 -0
  19. package/lib/cjs/simulate/authorizations/validate.js +23 -0
  20. package/lib/cjs/simulate/backends/eth-simulate-v1.d.ts +15 -0
  21. package/lib/cjs/simulate/backends/eth-simulate-v1.js +76 -0
  22. package/lib/cjs/simulate/backends/index.d.ts +2 -0
  23. package/lib/cjs/simulate/backends/index.js +7 -0
  24. package/lib/cjs/simulate/backends/tenderly-rest.d.ts +20 -0
  25. package/lib/cjs/simulate/backends/tenderly-rest.js +269 -0
  26. package/lib/cjs/simulate/index.d.ts +1 -0
  27. package/lib/cjs/simulate/index.js +5 -0
  28. package/lib/cjs/simulate/parsing/index.d.ts +1 -0
  29. package/lib/cjs/simulate/parsing/index.js +5 -0
  30. package/lib/cjs/simulate/parsing/transfers.d.ts +27 -0
  31. package/lib/cjs/simulate/parsing/transfers.js +153 -0
  32. package/lib/cjs/simulate/pipeline/build-simulation-txs.d.ts +17 -0
  33. package/lib/cjs/simulate/pipeline/build-simulation-txs.js +50 -0
  34. package/lib/cjs/simulate/pipeline/bundler-retention.d.ts +19 -0
  35. package/lib/cjs/simulate/pipeline/bundler-retention.js +90 -0
  36. package/lib/cjs/simulate/pipeline/execute-simulation.d.ts +25 -0
  37. package/lib/cjs/simulate/pipeline/execute-simulation.js +87 -0
  38. package/lib/cjs/simulate/pipeline/index.d.ts +4 -0
  39. package/lib/cjs/simulate/pipeline/index.js +11 -0
  40. package/lib/cjs/simulate/pipeline/resolve-chain.d.ts +14 -0
  41. package/lib/cjs/simulate/pipeline/resolve-chain.js +19 -0
  42. package/lib/cjs/simulate/pipeline/validate-input.d.ts +10 -0
  43. package/lib/cjs/simulate/pipeline/validate-input.js +55 -0
  44. package/lib/cjs/simulate/simulate.d.ts +26 -0
  45. package/lib/cjs/simulate/simulate.js +46 -0
  46. package/lib/cjs/types.d.ts +131 -0
  47. package/lib/cjs/types.js +2 -0
  48. package/lib/esm/errors.d.ts +50 -0
  49. package/lib/esm/errors.js +62 -0
  50. package/lib/esm/index.d.ts +4 -0
  51. package/lib/esm/index.js +5 -0
  52. package/lib/esm/package.json +1 -0
  53. package/lib/esm/screen-addresses/index.d.ts +1 -0
  54. package/lib/esm/screen-addresses/index.js +1 -0
  55. package/lib/esm/screen-addresses/sanctioned-addresses.d.ts +11 -0
  56. package/lib/esm/screen-addresses/sanctioned-addresses.js +74 -0
  57. package/lib/esm/screen-addresses/screen-addresses.d.ts +34 -0
  58. package/lib/esm/screen-addresses/screen-addresses.js +149 -0
  59. package/lib/esm/simulate/authorizations/index.d.ts +2 -0
  60. package/lib/esm/simulate/authorizations/index.js +2 -0
  61. package/lib/esm/simulate/authorizations/resolve.d.ts +9 -0
  62. package/lib/esm/simulate/authorizations/resolve.js +26 -0
  63. package/lib/esm/simulate/authorizations/validate.d.ts +6 -0
  64. package/lib/esm/simulate/authorizations/validate.js +20 -0
  65. package/lib/esm/simulate/backends/eth-simulate-v1.d.ts +15 -0
  66. package/lib/esm/simulate/backends/eth-simulate-v1.js +73 -0
  67. package/lib/esm/simulate/backends/index.d.ts +2 -0
  68. package/lib/esm/simulate/backends/index.js +2 -0
  69. package/lib/esm/simulate/backends/tenderly-rest.d.ts +20 -0
  70. package/lib/esm/simulate/backends/tenderly-rest.js +266 -0
  71. package/lib/esm/simulate/index.d.ts +1 -0
  72. package/lib/esm/simulate/index.js +1 -0
  73. package/lib/esm/simulate/parsing/index.d.ts +1 -0
  74. package/lib/esm/simulate/parsing/index.js +1 -0
  75. package/lib/esm/simulate/parsing/transfers.d.ts +27 -0
  76. package/lib/esm/simulate/parsing/transfers.js +149 -0
  77. package/lib/esm/simulate/pipeline/build-simulation-txs.d.ts +17 -0
  78. package/lib/esm/simulate/pipeline/build-simulation-txs.js +47 -0
  79. package/lib/esm/simulate/pipeline/bundler-retention.d.ts +19 -0
  80. package/lib/esm/simulate/pipeline/bundler-retention.js +87 -0
  81. package/lib/esm/simulate/pipeline/execute-simulation.d.ts +25 -0
  82. package/lib/esm/simulate/pipeline/execute-simulation.js +84 -0
  83. package/lib/esm/simulate/pipeline/index.d.ts +4 -0
  84. package/lib/esm/simulate/pipeline/index.js +4 -0
  85. package/lib/esm/simulate/pipeline/resolve-chain.d.ts +14 -0
  86. package/lib/esm/simulate/pipeline/resolve-chain.js +16 -0
  87. package/lib/esm/simulate/pipeline/validate-input.d.ts +10 -0
  88. package/lib/esm/simulate/pipeline/validate-input.js +52 -0
  89. package/lib/esm/simulate/simulate.d.ts +26 -0
  90. package/lib/esm/simulate/simulate.js +43 -0
  91. package/lib/esm/types.d.ts +131 -0
  92. package/lib/esm/types.js +1 -0
  93. package/package.json +49 -0
  94. package/src/index.ts +27 -0
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.simulateV1 = simulateV1;
4
+ const viem_1 = require("viem");
5
+ const errors_js_1 = require("../../errors.js");
6
+ /**
7
+ * Simulate transactions using eth_simulateV1 (viem simulateCalls).
8
+ *
9
+ * Uses stateOverride to set the user's ETH balance high enough to avoid
10
+ * false "insufficient balance for gas" reverts.
11
+ */
12
+ async function simulateV1(params) {
13
+ const { rpcUrl, transactions, blockNumber, signal } = params;
14
+ const client = (0, viem_1.createPublicClient)({
15
+ transport: (0, viem_1.http)(rpcUrl, {
16
+ fetchOptions: signal ? { signal } : undefined,
17
+ }),
18
+ });
19
+ const firstTx = transactions[0];
20
+ if (!firstTx) {
21
+ throw new errors_js_1.SimulationValidationError("At least one transaction is required", []);
22
+ }
23
+ const sender = (0, viem_1.getAddress)(firstTx.from);
24
+ // eth_simulateV1 executes all calls as the same account. Reject mixed senders
25
+ // to avoid silently simulating under the wrong address.
26
+ const mixedSender = transactions.find((tx) => (0, viem_1.getAddress)(tx.from) !== sender);
27
+ if (mixedSender) {
28
+ throw new errors_js_1.SimulationValidationError("All transactions must have the same from address for eth_simulateV1", [`expected ${sender}, got ${mixedSender.from}`]);
29
+ }
30
+ const calls = transactions.map((tx) => ({
31
+ to: tx.to,
32
+ data: tx.data,
33
+ value: tx.value,
34
+ }));
35
+ const blockParam = typeof blockNumber === "bigint"
36
+ ? { blockNumber }
37
+ : blockNumber !== undefined
38
+ ? { blockTag: blockNumber }
39
+ : {};
40
+ try {
41
+ const simulationResult = await client.simulateCalls({
42
+ account: sender,
43
+ calls,
44
+ ...blockParam,
45
+ // Inflate sender ETH balance to prevent false "insufficient gas" reverts.
46
+ // Without this, valid ERC20 flows fail when the sender has low ETH.
47
+ stateOverrides: [{ address: sender, balance: viem_1.maxUint256 }],
48
+ });
49
+ const results = simulationResult.results;
50
+ if (!Array.isArray(results)) {
51
+ throw new errors_js_1.ExternalServiceError("eth_simulateV1 returned unexpected response format");
52
+ }
53
+ const failedResult = results.find((r) => r.status !== "success");
54
+ if (failedResult) {
55
+ throw new errors_js_1.SimulationRevertedError(failedResult.error?.message ?? "Simulation failed", results);
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 };
68
+ }
69
+ catch (error) {
70
+ if (error instanceof errors_js_1.SimulationRevertedError)
71
+ throw error;
72
+ if (error instanceof errors_js_1.ExternalServiceError)
73
+ throw error;
74
+ throw new errors_js_1.ExternalServiceError(`eth_simulateV1 error: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
75
+ }
76
+ }
@@ -0,0 +1,2 @@
1
+ export { simulateV1 } from "./eth-simulate-v1.js";
2
+ export { simulateTenderlyRest } from "./tenderly-rest.js";
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.simulateTenderlyRest = exports.simulateV1 = void 0;
4
+ var eth_simulate_v1_js_1 = require("./eth-simulate-v1.js");
5
+ Object.defineProperty(exports, "simulateV1", { enumerable: true, get: function () { return eth_simulate_v1_js_1.simulateV1; } });
6
+ var tenderly_rest_js_1 = require("./tenderly-rest.js");
7
+ Object.defineProperty(exports, "simulateTenderlyRest", { enumerable: true, get: function () { return tenderly_rest_js_1.simulateTenderlyRest; } });
@@ -0,0 +1,20 @@
1
+ import { type BlockTag } from "viem";
2
+ import type { RawSimulationResult, SimulationLogger, SimulationTransaction, TenderlyRestConfig } from "../../types.js";
3
+ /**
4
+ * Simulate transactions using Tenderly REST API.
5
+ *
6
+ * @param shareable - true persists the simulation in Tenderly and returns a shareable URL;
7
+ * false runs ephemerally with no URL. Translates to Tenderly's `save` wire field
8
+ * (`save_if_fails` is also set to `shareable` — so failed preview simulations are
9
+ * ALSO persisted and shareable. Callers that preview sensitive calldata should be
10
+ * aware this persists in Tenderly's dashboard regardless of success/failure).
11
+ */
12
+ export declare function simulateTenderlyRest(params: {
13
+ config: TenderlyRestConfig;
14
+ chainId: number;
15
+ transactions: SimulationTransaction[];
16
+ blockNumber?: bigint | BlockTag;
17
+ shareable: boolean;
18
+ signal?: AbortSignal;
19
+ logger?: SimulationLogger;
20
+ }): Promise<RawSimulationResult>;
@@ -0,0 +1,269 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.simulateTenderlyRest = simulateTenderlyRest;
4
+ const viem_1 = require("viem");
5
+ const zod_1 = require("zod");
6
+ const errors_js_1 = require("../../errors.js");
7
+ // Zod validators for the subset of Tenderly responses we consume.
8
+ // Addresses must be 40-hex-digit `0x...` (not just any string); topics and
9
+ // data must be `0x`-prefixed hex so downstream `getAddress()` / `BigInt()`
10
+ // calls don't throw inside `parseTransfers`, silently dropping transfers.
11
+ const addressSchema = zod_1.z.custom((val) => typeof val === "string" && (0, viem_1.isAddress)(val));
12
+ const hexSchema = zod_1.z.custom((val) => typeof val === "string" && (0, viem_1.isHex)(val));
13
+ const tenderlyRawResponseSchema = zod_1.z
14
+ .object({
15
+ simulation: zod_1.z
16
+ .object({
17
+ id: zod_1.z.string().optional(),
18
+ status: zod_1.z.boolean().optional(),
19
+ error_message: zod_1.z.string().optional(),
20
+ })
21
+ .passthrough(),
22
+ transaction: zod_1.z
23
+ .object({
24
+ error_message: zod_1.z.string().optional(),
25
+ transaction_info: zod_1.z
26
+ .object({
27
+ logs: zod_1.z
28
+ .array(zod_1.z
29
+ .object({
30
+ raw: zod_1.z
31
+ .object({
32
+ address: addressSchema,
33
+ topics: zod_1.z.array(hexSchema).optional(),
34
+ data: hexSchema.optional(),
35
+ })
36
+ .passthrough()
37
+ .optional(),
38
+ })
39
+ .passthrough())
40
+ .optional(),
41
+ asset_changes: zod_1.z.unknown().optional(),
42
+ })
43
+ .passthrough(),
44
+ })
45
+ .passthrough(),
46
+ })
47
+ .passthrough();
48
+ const tenderlyBundleRawResponseSchema = zod_1.z
49
+ .object({
50
+ simulation_results: zod_1.z.array(tenderlyRawResponseSchema).min(1),
51
+ })
52
+ .passthrough();
53
+ /**
54
+ * Simulate transactions using Tenderly REST API.
55
+ *
56
+ * @param shareable - true persists the simulation in Tenderly and returns a shareable URL;
57
+ * false runs ephemerally with no URL. Translates to Tenderly's `save` wire field
58
+ * (`save_if_fails` is also set to `shareable` — so failed preview simulations are
59
+ * ALSO persisted and shareable. Callers that preview sensitive calldata should be
60
+ * aware this persists in Tenderly's dashboard regardless of success/failure).
61
+ */
62
+ async function simulateTenderlyRest(params) {
63
+ const { config, chainId, transactions, blockNumber, shareable, signal, logger, } = params;
64
+ const baseUrl = buildBaseUrl(config);
65
+ try {
66
+ if (transactions.length === 1) {
67
+ return await simulateSingle({
68
+ baseUrl,
69
+ config,
70
+ chainId,
71
+ tx: transactions[0],
72
+ blockNumber,
73
+ shareable,
74
+ signal,
75
+ logger,
76
+ });
77
+ }
78
+ return await simulateBundle({
79
+ baseUrl,
80
+ config,
81
+ chainId,
82
+ transactions,
83
+ blockNumber,
84
+ shareable,
85
+ signal,
86
+ logger,
87
+ });
88
+ }
89
+ catch (error) {
90
+ if (error instanceof errors_js_1.SimulationRevertedError)
91
+ throw error;
92
+ if (error instanceof errors_js_1.ExternalServiceError)
93
+ throw error;
94
+ throw new errors_js_1.ExternalServiceError(`Tenderly REST API error: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
95
+ }
96
+ }
97
+ /** URL-encode the path-segment config values so `/`, `?`, `#` can't retarget the request. */
98
+ function buildBaseUrl(config) {
99
+ const account = encodeURIComponent(config.accountSlug);
100
+ const project = encodeURIComponent(config.projectSlug);
101
+ return `${config.apiBaseUrl}/api/v1/account/${account}/project/${project}`;
102
+ }
103
+ /** Build the `TenderlySimulateRequest` body for one transaction. Shared by single + bundle. */
104
+ function buildTxBody(tx, chainId, shareable, blockNumber) {
105
+ return {
106
+ network_id: String(chainId),
107
+ from: tx.from,
108
+ to: tx.to,
109
+ input: tx.data,
110
+ value: String(tx.value ?? 0n),
111
+ save: shareable,
112
+ save_if_fails: shareable,
113
+ simulation_type: "full",
114
+ ...(blockNumber !== undefined && {
115
+ block_number: typeof blockNumber === "bigint" ? String(blockNumber) : blockNumber,
116
+ }),
117
+ };
118
+ }
119
+ async function simulateSingle(params) {
120
+ const { baseUrl, config, chainId, tx, blockNumber, shareable, signal, logger, } = params;
121
+ const body = buildTxBody(tx, chainId, shareable, blockNumber);
122
+ const response = await fetch(`${baseUrl}/simulate`, {
123
+ method: "POST",
124
+ headers: {
125
+ "Content-Type": "application/json",
126
+ "X-Access-Key": config.accessToken,
127
+ },
128
+ body: JSON.stringify(body),
129
+ signal,
130
+ });
131
+ if (!response.ok) {
132
+ throw new errors_js_1.ExternalServiceError(`Tenderly REST API returned ${response.status}: ${response.statusText}`);
133
+ }
134
+ 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;
148
+ }
149
+ async function simulateBundle(params) {
150
+ const { baseUrl, config, chainId, transactions, blockNumber, shareable, signal, logger, } = params;
151
+ const body = {
152
+ simulations: transactions.map((tx) => buildTxBody(tx, chainId, shareable, blockNumber)),
153
+ };
154
+ const response = await fetch(`${baseUrl}/simulate-bundle`, {
155
+ method: "POST",
156
+ headers: {
157
+ "Content-Type": "application/json",
158
+ "X-Access-Key": config.accessToken,
159
+ },
160
+ body: JSON.stringify(body),
161
+ signal,
162
+ });
163
+ if (!response.ok) {
164
+ throw new errors_js_1.ExternalServiceError(`Tenderly REST API returned ${response.status}: ${response.statusText}`);
165
+ }
166
+ const data = tenderlyBundleRawResponseSchema.parse(await response.json());
167
+ 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
+ };
201
+ }
202
+ /**
203
+ * Try to make a simulation publicly shareable. Returns true iff Tenderly's
204
+ * `/share` endpoint responded 2xx. On any other outcome (non-ok response,
205
+ * abort, network error), logs via `logger` and returns false — the caller
206
+ * then clears `tenderlyUrl`. `AbortError` is re-thrown so cancellation
207
+ * propagates cleanly instead of silently downgrading to "no URL".
208
+ */
209
+ async function shareSimulation(params) {
210
+ const { baseUrl, config, simulationId, signal, logger } = params;
211
+ const safeId = encodeURIComponent(simulationId);
212
+ try {
213
+ const response = await fetch(`${baseUrl}/simulations/${safeId}/share`, {
214
+ method: "POST",
215
+ headers: { "X-Access-Key": config.accessToken },
216
+ signal,
217
+ });
218
+ if (!response.ok) {
219
+ logger?.warn("Tenderly /share returned non-ok; tenderlyUrl will be cleared", {
220
+ simulationId,
221
+ status: response.status,
222
+ statusText: response.statusText,
223
+ });
224
+ return false;
225
+ }
226
+ return true;
227
+ }
228
+ catch (error) {
229
+ // Propagate cancellation; swallow only genuine fetch-layer failures.
230
+ if (error instanceof Error && error.name === "AbortError")
231
+ throw error;
232
+ logger?.warn("Tenderly /share fetch failed; tenderlyUrl will be cleared", {
233
+ simulationId,
234
+ error: error instanceof Error ? error.message : String(error),
235
+ });
236
+ return false;
237
+ }
238
+ }
239
+ function parseTenderlyResponse(data, shareable) {
240
+ const { simulation, transaction } = data;
241
+ if (simulation.status !== true) {
242
+ const errorMessage = transaction.error_message ||
243
+ simulation.error_message ||
244
+ "Transaction simulation reverted";
245
+ // NOTE: `data` includes the full Tenderly response (request body, calldata,
246
+ // asset changes). Callers that log `error.details` should redact — see
247
+ // `errors.ts` doc for `SimulationRevertedError.details`.
248
+ throw new errors_js_1.SimulationRevertedError(errorMessage, data);
249
+ }
250
+ const rawLogs = [];
251
+ const logs = transaction.transaction_info.logs ?? [];
252
+ for (const log of logs) {
253
+ if (log.raw) {
254
+ rawLogs.push({
255
+ address: log.raw.address,
256
+ topics: log.raw.topics ?? [],
257
+ data: log.raw.data ?? "0x",
258
+ });
259
+ }
260
+ }
261
+ const tenderlyUrl = shareable && simulation.id
262
+ ? `https://dashboard.tenderly.co/shared/simulation/${simulation.id}`
263
+ : undefined;
264
+ return {
265
+ logs: rawLogs,
266
+ tenderlyUrl,
267
+ rawAssetChanges: transaction.transaction_info.asset_changes,
268
+ };
269
+ }
@@ -0,0 +1 @@
1
+ export { simulate } from "./simulate.js";
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.simulate = void 0;
4
+ var simulate_js_1 = require("./simulate.js");
5
+ Object.defineProperty(exports, "simulate", { enumerable: true, get: function () { return simulate_js_1.simulate; } });
@@ -0,0 +1 @@
1
+ export { parseTransfers } from "./transfers.js";
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseTransfers = void 0;
4
+ var transfers_js_1 = require("./transfers.js");
5
+ Object.defineProperty(exports, "parseTransfers", { enumerable: true, get: function () { return transfers_js_1.parseTransfers; } });
@@ -0,0 +1,27 @@
1
+ import type { RawLog, SimulationLogger, Transfer } from "../../types.js";
2
+ export declare const TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
3
+ export declare const WITHDRAWAL_TOPIC = "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65";
4
+ export declare const DEPOSIT_TOPIC = "0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c";
5
+ /**
6
+ * Parse raw EVM logs into individual Transfer events.
7
+ *
8
+ * **Supported event types:** ERC20 `Transfer(from, to, amount)` and WETH9
9
+ * `Deposit(to, amount)` / `Withdrawal(from, amount)`. WETH9 mint/burn Transfer
10
+ * events paired with their Deposit/Withdrawal are deduplicated. ERC721 and
11
+ * ERC1155 transfer events are **not** parsed — consumers with NFT flows will
12
+ * see an incomplete transfer list.
13
+ *
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.
18
+ *
19
+ * **Failure mode:** on a per-log parse failure (malformed topic length,
20
+ * 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
+ *
25
+ * Output is sorted canonically by token, from, to, amount for determinism.
26
+ */
27
+ export declare function parseTransfers(logs: RawLog[], logger?: SimulationLogger): Transfer[];
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEPOSIT_TOPIC = exports.WITHDRAWAL_TOPIC = exports.TRANSFER_TOPIC = void 0;
4
+ exports.parseTransfers = parseTransfers;
5
+ const viem_1 = require("viem");
6
+ // keccak256("Transfer(address,address,uint256)") — ERC-20 transfer event
7
+ exports.TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
8
+ // keccak256("Withdrawal(address,uint256)") — WETH9 unwrap event
9
+ exports.WITHDRAWAL_TOPIC = "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65";
10
+ // keccak256("Deposit(address,uint256)") — WETH9 wrap event
11
+ exports.DEPOSIT_TOPIC = "0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c";
12
+ const TOPIC_HEX_LENGTH = 66; // "0x" + 32 bytes
13
+ const UINT256_HEX_LENGTH = 66; // "0x" + 32 bytes
14
+ /**
15
+ * Parse raw EVM logs into individual Transfer events.
16
+ *
17
+ * **Supported event types:** ERC20 `Transfer(from, to, amount)` and WETH9
18
+ * `Deposit(to, amount)` / `Withdrawal(from, amount)`. WETH9 mint/burn Transfer
19
+ * events paired with their Deposit/Withdrawal are deduplicated. ERC721 and
20
+ * ERC1155 transfer events are **not** parsed — consumers with NFT flows will
21
+ * see an incomplete transfer list.
22
+ *
23
+ * **WETH9-specific dedup:** matches Transfer-to/from zero against the
24
+ * matching Deposit/Withdrawal event on the same contract with the same data
25
+ * and withdrawer. Non-standard wrapped-native implementations (e.g., rebasing
26
+ * or fee-on-withdraw wrappers) may escape the dedup and produce double-counts.
27
+ *
28
+ * **Failure mode:** on a per-log parse failure (malformed topic length,
29
+ * non-hex data), the log is skipped and a `warn` is emitted via the logger.
30
+ * The caller receives a possibly-incomplete transfer list. Upstream sources
31
+ * (e.g., Tenderly response schemas) should validate log shape before calling
32
+ * here to avoid silent drops.
33
+ *
34
+ * Output is sorted canonically by token, from, to, amount for determinism.
35
+ */
36
+ function parseTransfers(logs, logger) {
37
+ const transfers = [];
38
+ for (const log of logs) {
39
+ try {
40
+ const topic0 = log.topics[0];
41
+ if (topic0 === undefined)
42
+ continue;
43
+ switch (topic0) {
44
+ case exports.WITHDRAWAL_TOPIC: {
45
+ const fromTopic = log.topics[1];
46
+ if (!isTopicHex(fromTopic) || !isUint256Hex(log.data)) {
47
+ warnMalformed(logger, log, "WETH9 Withdrawal: bad topic[1] or data length");
48
+ continue;
49
+ }
50
+ transfers.push({
51
+ token: (0, viem_1.getAddress)(log.address),
52
+ from: (0, viem_1.getAddress)(`0x${fromTopic.slice(26)}`),
53
+ to: viem_1.zeroAddress,
54
+ amount: BigInt(log.data),
55
+ });
56
+ continue;
57
+ }
58
+ case exports.DEPOSIT_TOPIC: {
59
+ const toTopic = log.topics[1];
60
+ if (!isTopicHex(toTopic) || !isUint256Hex(log.data)) {
61
+ warnMalformed(logger, log, "WETH9 Deposit: bad topic[1] or data length");
62
+ continue;
63
+ }
64
+ transfers.push({
65
+ token: (0, viem_1.getAddress)(log.address),
66
+ from: viem_1.zeroAddress,
67
+ to: (0, viem_1.getAddress)(`0x${toTopic.slice(26)}`),
68
+ amount: BigInt(log.data),
69
+ });
70
+ continue;
71
+ }
72
+ case exports.TRANSFER_TOPIC: {
73
+ // ERC-20 Transfer has 3 topics; ERC-721 Transfer has 4 (tokenId indexed).
74
+ // ERC-721 flows are out of scope.
75
+ if (log.topics.length !== 3)
76
+ continue;
77
+ const fromTopic = log.topics[1];
78
+ const toTopic = log.topics[2];
79
+ if (!isTopicHex(fromTopic) ||
80
+ !isTopicHex(toTopic) ||
81
+ !isUint256Hex(log.data)) {
82
+ warnMalformed(logger, log, "ERC20 Transfer: bad topic/data length");
83
+ continue;
84
+ }
85
+ // WETH9 unwrap dedup: Transfer to zero paired with a Withdrawal of equal amount.
86
+ if (toTopic === viem_1.zeroHash &&
87
+ logs.some((other) => other.topics[0] === exports.WITHDRAWAL_TOPIC &&
88
+ other.address === log.address &&
89
+ other.data === log.data &&
90
+ other.topics.length === 2 &&
91
+ other.topics[1] === fromTopic))
92
+ continue;
93
+ // WETH9 wrap dedup: Transfer from zero paired with a Deposit of equal amount.
94
+ if (fromTopic === viem_1.zeroHash &&
95
+ logs.some((other) => other.topics[0] === exports.DEPOSIT_TOPIC &&
96
+ other.address === log.address &&
97
+ other.data === log.data &&
98
+ other.topics.length === 2 &&
99
+ other.topics[1] === toTopic))
100
+ continue;
101
+ transfers.push({
102
+ token: (0, viem_1.getAddress)(log.address),
103
+ from: (0, viem_1.getAddress)(`0x${fromTopic.slice(26)}`),
104
+ to: (0, viem_1.getAddress)(`0x${toTopic.slice(26)}`),
105
+ amount: BigInt(log.data),
106
+ });
107
+ continue;
108
+ }
109
+ }
110
+ }
111
+ catch (error) {
112
+ // Safety net for anything that slips past the length checks above
113
+ // (e.g., viem checksum throws on non-hex input). Loud enough to diagnose.
114
+ warnMalformed(logger, log, error instanceof Error ? error.message : String(error));
115
+ }
116
+ }
117
+ return sortTransfers(transfers);
118
+ }
119
+ function isTopicHex(value) {
120
+ return typeof value === "string" && value.length === TOPIC_HEX_LENGTH;
121
+ }
122
+ function isUint256Hex(value) {
123
+ return typeof value === "string" && value.length === UINT256_HEX_LENGTH;
124
+ }
125
+ function warnMalformed(logger, log, reason) {
126
+ logger?.warn("Skipping malformed log during transfer parsing", {
127
+ address: log.address,
128
+ topics: log.topics,
129
+ reason,
130
+ });
131
+ }
132
+ /**
133
+ * Canonical sort: by lowercased token, from, to, then amount. Addresses are
134
+ * pre-lowercased once to avoid redundant `.toLowerCase()` calls per comparison.
135
+ */
136
+ function sortTransfers(transfers) {
137
+ const decorated = transfers.map((t) => ({
138
+ t,
139
+ tokenLc: t.token.toLowerCase(),
140
+ fromLc: t.from.toLowerCase(),
141
+ toLc: t.to.toLowerCase(),
142
+ }));
143
+ decorated.sort((a, b) => {
144
+ if (a.tokenLc !== b.tokenLc)
145
+ return a.tokenLc < b.tokenLc ? -1 : 1;
146
+ if (a.fromLc !== b.fromLc)
147
+ return a.fromLc < b.fromLc ? -1 : 1;
148
+ if (a.toLc !== b.toLc)
149
+ return a.toLc < b.toLc ? -1 : 1;
150
+ return a.t.amount < b.t.amount ? -1 : a.t.amount > b.t.amount ? 1 : 0;
151
+ });
152
+ return decorated.map((d) => d.t);
153
+ }
@@ -0,0 +1,17 @@
1
+ import type { SimulateParams, SimulationTransaction } from "../../types.js";
2
+ /**
3
+ * Stage 3 of the simulate() pipeline.
4
+ *
5
+ * Prepends any resolved authorizations (e.g. ERC20 `approve` calls generated from
6
+ * `{ type: 'signature' }` entries) to the user's transactions. If no authorizations
7
+ * were supplied, returns the user's transactions unchanged.
8
+ *
9
+ * Defense-in-depth: after prepending, verifies EVERY resolved tx shares the user
10
+ * sender. This catches an `{ type: 'approval', transaction }` auth where the
11
+ * caller-provided `transaction.from` does not match the bundle sender — the
12
+ * `approval` variant otherwise passes through without validation. Rejecting here
13
+ * keeps the "all simulationTxs share one `from`" invariant the downstream
14
+ * backends (eth_simulateV1) assume, and prevents a malicious or buggy caller
15
+ * from prepending a tx on behalf of an unexpected address.
16
+ */
17
+ export declare function buildSimulationTxs(params: SimulateParams): SimulationTransaction[];
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildSimulationTxs = buildSimulationTxs;
4
+ const viem_1 = require("viem");
5
+ const errors_js_1 = require("../../errors.js");
6
+ const index_js_1 = require("../authorizations/index.js");
7
+ /**
8
+ * Stage 3 of the simulate() pipeline.
9
+ *
10
+ * Prepends any resolved authorizations (e.g. ERC20 `approve` calls generated from
11
+ * `{ type: 'signature' }` entries) to the user's transactions. If no authorizations
12
+ * were supplied, returns the user's transactions unchanged.
13
+ *
14
+ * Defense-in-depth: after prepending, verifies EVERY resolved tx shares the user
15
+ * sender. This catches an `{ type: 'approval', transaction }` auth where the
16
+ * caller-provided `transaction.from` does not match the bundle sender — the
17
+ * `approval` variant otherwise passes through without validation. Rejecting here
18
+ * keeps the "all simulationTxs share one `from`" invariant the downstream
19
+ * backends (eth_simulateV1) assume, and prevents a malicious or buggy caller
20
+ * from prepending a tx on behalf of an unexpected address.
21
+ */
22
+ function buildSimulationTxs(params) {
23
+ const { transactions, authorizations } = params;
24
+ if (!authorizations || authorizations.length === 0) {
25
+ return transactions;
26
+ }
27
+ const sender = transactions[0].from;
28
+ const resolved = (0, index_js_1.resolveAuthorizations)(authorizations, sender);
29
+ const simulationTxs = [...resolved, ...transactions];
30
+ const senderChecksum = (0, viem_1.getAddress)(sender);
31
+ const mismatches = [];
32
+ for (let i = 0; i < simulationTxs.length; i++) {
33
+ const tx = simulationTxs[i];
34
+ let txFromChecksum;
35
+ try {
36
+ txFromChecksum = (0, viem_1.getAddress)(tx.from);
37
+ }
38
+ catch {
39
+ mismatches.push(`simulationTxs[${i}].from: not a valid address (${tx.from})`);
40
+ continue;
41
+ }
42
+ if (txFromChecksum !== senderChecksum) {
43
+ mismatches.push(`simulationTxs[${i}].from: expected ${senderChecksum}, got ${txFromChecksum}`);
44
+ }
45
+ }
46
+ if (mismatches.length > 0) {
47
+ throw new errors_js_1.SimulationValidationError("Authorization transaction(s) must share the user sender", mismatches);
48
+ }
49
+ return simulationTxs;
50
+ }
@@ -0,0 +1,19 @@
1
+ import type { SimulationLogger, Transfer } from "../../types.js";
2
+ interface AssertNoBundlerRetentionParams {
3
+ chainId: number;
4
+ transfers: Transfer[];
5
+ logger?: SimulationLogger;
6
+ }
7
+ /**
8
+ * Assert that no value is retained by bundler3 contract addresses.
9
+ *
10
+ * Uses net flow (inbound minus outbound) per (bundler address, token) pair.
11
+ * Bundler3 legitimately receives tokens as an intermediary (user → bundler3 →
12
+ * vault), so gross inbound would fire false positives. We only flag when
13
+ * `|net| > DUST_THRESHOLD` — catching both **retention** (tokens stuck in
14
+ * bundler, positive net) and **drain** (bundler sending more than it received
15
+ * in this bundle, negative net — which implies a pre-existing or state-override
16
+ * balance is being drawn down, a red flag for pre-broadcast integrity).
17
+ */
18
+ export declare function assertNoBundlerRetention(params: AssertNoBundlerRetentionParams): void;
19
+ export {};