@morpho-org/evm-simulation 2.0.2 → 3.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 (51) 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/backends/index.d.ts +1 -1
  7. package/lib/cjs/simulate/backends/index.js +3 -3
  8. package/lib/cjs/simulate/backends/tenderly-rpc.d.ts +23 -0
  9. package/lib/cjs/simulate/backends/tenderly-rpc.js +176 -0
  10. package/lib/cjs/simulate/parsing/transfers.js +3 -1
  11. package/lib/cjs/simulate/pipeline/execute-simulation.d.ts +6 -10
  12. package/lib/cjs/simulate/pipeline/execute-simulation.js +11 -18
  13. package/lib/cjs/simulate/pipeline/resolve-chain.d.ts +6 -5
  14. package/lib/cjs/simulate/pipeline/resolve-chain.js +10 -7
  15. package/lib/cjs/simulate/simulate.d.ts +16 -21
  16. package/lib/cjs/simulate/simulate.js +16 -18
  17. package/lib/cjs/types.d.ts +22 -29
  18. package/lib/esm/errors.d.ts +0 -6
  19. package/lib/esm/errors.js +0 -9
  20. package/lib/esm/index.d.ts +2 -3
  21. package/lib/esm/index.js +1 -2
  22. package/lib/esm/simulate/backends/index.d.ts +1 -1
  23. package/lib/esm/simulate/backends/index.js +1 -1
  24. package/lib/esm/simulate/backends/tenderly-rpc.d.ts +23 -0
  25. package/lib/esm/simulate/backends/tenderly-rpc.js +173 -0
  26. package/lib/esm/simulate/parsing/transfers.js +3 -1
  27. package/lib/esm/simulate/pipeline/execute-simulation.d.ts +6 -10
  28. package/lib/esm/simulate/pipeline/execute-simulation.js +12 -19
  29. package/lib/esm/simulate/pipeline/resolve-chain.d.ts +6 -5
  30. package/lib/esm/simulate/pipeline/resolve-chain.js +10 -7
  31. package/lib/esm/simulate/simulate.d.ts +16 -21
  32. package/lib/esm/simulate/simulate.js +16 -18
  33. package/lib/esm/types.d.ts +22 -29
  34. package/package.json +3 -3
  35. package/src/index.ts +1 -3
  36. package/lib/cjs/screen-addresses/index.d.ts +0 -1
  37. package/lib/cjs/screen-addresses/index.js +0 -5
  38. package/lib/cjs/screen-addresses/sanctioned-addresses.d.ts +0 -11
  39. package/lib/cjs/screen-addresses/sanctioned-addresses.js +0 -77
  40. package/lib/cjs/screen-addresses/screen-addresses.d.ts +0 -50
  41. package/lib/cjs/screen-addresses/screen-addresses.js +0 -171
  42. package/lib/cjs/simulate/backends/tenderly-rest.d.ts +0 -20
  43. package/lib/cjs/simulate/backends/tenderly-rest.js +0 -279
  44. package/lib/esm/screen-addresses/index.d.ts +0 -1
  45. package/lib/esm/screen-addresses/index.js +0 -1
  46. package/lib/esm/screen-addresses/sanctioned-addresses.d.ts +0 -11
  47. package/lib/esm/screen-addresses/sanctioned-addresses.js +0 -74
  48. package/lib/esm/screen-addresses/screen-addresses.d.ts +0 -50
  49. package/lib/esm/screen-addresses/screen-addresses.js +0 -168
  50. package/lib/esm/simulate/backends/tenderly-rest.d.ts +0 -20
  51. package/lib/esm/simulate/backends/tenderly-rest.js +0 -276
@@ -1,279 +0,0 @@
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
- gas_used: zod_1.z.number(),
26
- transaction_info: zod_1.z
27
- .object({
28
- logs: zod_1.z
29
- .array(zod_1.z
30
- .object({
31
- raw: zod_1.z
32
- .object({
33
- address: addressSchema,
34
- topics: zod_1.z.array(hexSchema).optional(),
35
- data: hexSchema.optional(),
36
- })
37
- .passthrough()
38
- .optional(),
39
- })
40
- .passthrough())
41
- .optional(),
42
- asset_changes: zod_1.z.unknown().optional(),
43
- call_trace: zod_1.z
44
- .object({
45
- output: hexSchema,
46
- })
47
- .passthrough(),
48
- })
49
- .passthrough(),
50
- })
51
- .passthrough(),
52
- })
53
- .passthrough();
54
- const tenderlyBundleRawResponseSchema = zod_1.z
55
- .object({
56
- simulation_results: zod_1.z.array(tenderlyRawResponseSchema).min(1),
57
- })
58
- .passthrough();
59
- /**
60
- * Simulate transactions using Tenderly REST API.
61
- *
62
- * @param shareable - true persists the simulation in Tenderly and returns a shareable URL;
63
- * false runs ephemerally with no URL. Translates to Tenderly's `save` wire field
64
- * (`save_if_fails` is also set to `shareable` — so failed preview simulations are
65
- * ALSO persisted and shareable. Callers that preview sensitive calldata should be
66
- * aware this persists in Tenderly's dashboard regardless of success/failure).
67
- */
68
- async function simulateTenderlyRest(params) {
69
- const { config, chainId, transactions, blockNumber, shareable, signal, logger, } = params;
70
- const baseUrl = buildBaseUrl(config);
71
- try {
72
- if (transactions.length === 1) {
73
- return await simulateSingle({
74
- baseUrl,
75
- config,
76
- chainId,
77
- tx: transactions[0],
78
- blockNumber,
79
- shareable,
80
- signal,
81
- logger,
82
- });
83
- }
84
- return await simulateBundle({
85
- baseUrl,
86
- config,
87
- chainId,
88
- transactions,
89
- blockNumber,
90
- shareable,
91
- signal,
92
- logger,
93
- });
94
- }
95
- catch (error) {
96
- if (error instanceof errors_js_1.SimulationRevertedError)
97
- throw error;
98
- if (error instanceof errors_js_1.ExternalServiceError)
99
- throw error;
100
- throw new errors_js_1.ExternalServiceError(`Tenderly REST API error: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
101
- }
102
- }
103
- /** URL-encode the path-segment config values so `/`, `?`, `#` can't retarget the request. */
104
- function buildBaseUrl(config) {
105
- const account = encodeURIComponent(config.accountSlug);
106
- const project = encodeURIComponent(config.projectSlug);
107
- return `${config.apiBaseUrl}/api/v1/account/${account}/project/${project}`;
108
- }
109
- /** Build the `TenderlySimulateRequest` body for one transaction. Shared by single + bundle. */
110
- // biome-ignore lint/complexity/useMaxParams: TODO refactor to ≤2 params
111
- function buildTxBody(tx, chainId, shareable, blockNumber) {
112
- return {
113
- network_id: String(chainId),
114
- from: tx.from,
115
- to: tx.to,
116
- input: tx.data,
117
- value: String(tx.value ?? 0n),
118
- save: shareable,
119
- save_if_fails: shareable,
120
- simulation_type: "full",
121
- ...(blockNumber !== undefined && {
122
- block_number: typeof blockNumber === "bigint" ? String(blockNumber) : blockNumber,
123
- }),
124
- };
125
- }
126
- async function simulateSingle(params) {
127
- const { baseUrl, config, chainId, tx, blockNumber, shareable, signal, logger, } = params;
128
- const body = buildTxBody(tx, chainId, shareable, blockNumber);
129
- const response = await fetch(`${baseUrl}/simulate`, {
130
- method: "POST",
131
- headers: {
132
- "Content-Type": "application/json",
133
- "X-Access-Key": config.accessToken,
134
- },
135
- body: JSON.stringify(body),
136
- signal,
137
- });
138
- if (!response.ok) {
139
- throw new errors_js_1.ExternalServiceError(`Tenderly REST API returned ${response.status}: ${response.statusText}`);
140
- }
141
- const data = tenderlyRawResponseSchema.parse(await response.json());
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 };
152
- }
153
- async function simulateBundle(params) {
154
- const { baseUrl, config, chainId, transactions, blockNumber, shareable, signal, logger, } = params;
155
- const body = {
156
- simulations: transactions.map((tx) => buildTxBody(tx, chainId, shareable, blockNumber)),
157
- };
158
- const response = await fetch(`${baseUrl}/simulate-bundle`, {
159
- method: "POST",
160
- headers: {
161
- "Content-Type": "application/json",
162
- "X-Access-Key": config.accessToken,
163
- },
164
- body: JSON.stringify(body),
165
- signal,
166
- });
167
- if (!response.ok) {
168
- throw new errors_js_1.ExternalServiceError(`Tenderly REST API returned ${response.status}: ${response.statusText}`);
169
- }
170
- const data = tenderlyBundleRawResponseSchema.parse(await response.json());
171
- const simulations = data.simulation_results;
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;
206
- }
207
- /**
208
- * Try to make a simulation publicly shareable. Returns true iff Tenderly's
209
- * `/share` endpoint responded 2xx. On any other outcome (non-ok response,
210
- * abort, network error), logs via `logger` and returns false — the caller
211
- * then clears `tenderlyUrl`. `AbortError` is re-thrown so cancellation
212
- * propagates cleanly instead of silently downgrading to "no URL".
213
- */
214
- async function shareSimulation(params) {
215
- const { baseUrl, config, simulationId, signal, logger } = params;
216
- const safeId = encodeURIComponent(simulationId);
217
- try {
218
- const response = await fetch(`${baseUrl}/simulations/${safeId}/share`, {
219
- method: "POST",
220
- headers: { "X-Access-Key": config.accessToken },
221
- signal,
222
- });
223
- if (!response.ok) {
224
- logger?.warn("Tenderly /share returned non-ok; tenderlyUrl will be cleared", {
225
- simulationId,
226
- status: response.status,
227
- statusText: response.statusText,
228
- });
229
- return false;
230
- }
231
- return true;
232
- }
233
- catch (error) {
234
- // Propagate cancellation; swallow only genuine fetch-layer failures.
235
- if (error instanceof Error && error.name === "AbortError")
236
- throw error;
237
- logger?.warn("Tenderly /share fetch failed; tenderlyUrl will be cleared", {
238
- simulationId,
239
- error: error instanceof Error ? error.message : String(error),
240
- });
241
- return false;
242
- }
243
- }
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) {
251
- const { simulation, transaction } = data;
252
- if (simulation.status !== true) {
253
- const errorMessage = transaction.error_message ||
254
- simulation.error_message ||
255
- "Transaction simulation reverted";
256
- // NOTE: `data` includes the full Tenderly response (request body, calldata,
257
- // asset changes). Callers that log `error.details` should redact — see
258
- // `errors.ts` doc for `SimulationRevertedError.details`.
259
- throw new errors_js_1.SimulationRevertedError(errorMessage, data);
260
- }
261
- const rawLogs = [];
262
- const logs = transaction.transaction_info.logs ?? [];
263
- for (const log of logs) {
264
- if (log.raw) {
265
- rawLogs.push({
266
- address: log.raw.address,
267
- topics: log.raw.topics ?? [],
268
- data: log.raw.data ?? "0x",
269
- });
270
- }
271
- }
272
- return {
273
- logs: rawLogs,
274
- status: true,
275
- returnData: transaction.transaction_info.call_trace.output,
276
- gasUsed: BigInt(transaction.gas_used),
277
- assetChanges: transaction.transaction_info.asset_changes,
278
- };
279
- }
@@ -1 +0,0 @@
1
- export { screenAddresses } from "./screen-addresses.js";
@@ -1 +0,0 @@
1
- export { screenAddresses } from "./screen-addresses.js";
@@ -1,11 +0,0 @@
1
- /**
2
- * Static list of sanctioned/compromised addresses.
3
- * Ported from offchain-services-server/src/functions/screen/blacklist.ts.
4
- * These addresses are checked before calling the Chainalysis API (fast, no network).
5
- *
6
- * **Invariant** (enforced at module load, see below): every entry is a
7
- * well-formed lowercased EVM address. `screenAddresses` does
8
- * `sanctionedAddresses.has(addr.toLowerCase())` — a mixed-case entry would
9
- * never match and would silently bypass the sanctioned check.
10
- */
11
- export declare const sanctionedAddresses: Set<string>;
@@ -1,74 +0,0 @@
1
- /**
2
- * Static list of sanctioned/compromised addresses.
3
- * Ported from offchain-services-server/src/functions/screen/blacklist.ts.
4
- * These addresses are checked before calling the Chainalysis API (fast, no network).
5
- *
6
- * **Invariant** (enforced at module load, see below): every entry is a
7
- * well-formed lowercased EVM address. `screenAddresses` does
8
- * `sanctionedAddresses.has(addr.toLowerCase())` — a mixed-case entry would
9
- * never match and would silently bypass the sanctioned check.
10
- */
11
- export const sanctionedAddresses = new Set([
12
- // Bybit hack addresses
13
- "0x47666Fab8bd0Ac7003bce3f5C3585383F09486E2",
14
- "0xA4B2Fd68593B6F34E51cB9eDB66E71c1B4Ab449e",
15
- "0x36ed3C0213565530C35115d93A80F9c04d94E4Cb",
16
- "0x1542368a03ad1f03d96D51B414f4738961Cf4443",
17
- "0xdD90071D52F20e85c89802e5Dc1eC0A7B6475f92",
18
- "0x40e98FeEEbaD7Ddb0F0534Ccaa617427eA10187e",
19
- "0x30a822CDD2782D2B2A12a08526452e885978FA1D",
20
- "0x6d46bd3AfF100f23C194e5312f93507978a6DC91",
21
- "0x660BfcEa3A5FAF823e8f8bF57dd558db034dea1d",
22
- "0x140c9Ab92347734641b1A7c124ffDeE58c20C3E3",
23
- "0x8c7235e1A6EeF91b980D0FcA083347FBb7EE1806",
24
- "0xb172F7e99452446f18FF49A71bfEeCf0873003b4",
25
- "0xCd7eC020121Ead6f99855cbB972dF502dB5bC63a",
26
- "0x0e8C1E2881F35Ef20343264862A242FB749d6b35",
27
- "0x2290937A4498C96eFfb87b8371a33D108F8D433f",
28
- "0x1bb0970508316DC735329752a4581E0a4bAbc6B4",
29
- "0xf0a16603289eAF35F64077Ba3681af41194a1c09",
30
- "0x5Af75eAB6BEC227657fA3E749a8BFd55f02e4b1D",
31
- "0xB4a862A81aBB2f952FcA4C6f5510962e18c7f1A2",
32
- "0x959c4CA19c4532C97A657D82d97acCBAb70e6fb4",
33
- "0xB72334cB9D0b614D30C4c60e2bd12fF5Ed03c305",
34
- "0xFc926659Dd8808f6e3e0a8d61B20B871F3Fa6465",
35
- "0xfa3FcCCB897079fD83bfBA690E7D47Eb402d6c49",
36
- "0x51E9d833Ecae4E8D9D8Be17300AEE6D3398C135D",
37
- "0x1eB27f136BFe7947f80d6ceE3Cf0bfDf92b45e57",
38
- "0x83Ef5E80faD88288F770152875Ab0bb16641a09E",
39
- "0x52207Ec7B1b43AA5DB116931a904371ae2C1619e",
40
- "0x23Db729908137cb60852f2936D2b5c6De0e1c887",
41
- "0xAF620E6d32B1c67f3396EF5d2F7d7642Dc2e6CE9",
42
- "0xCd1a4A457cA8b0931c3BF81Df3CFa227ADBdb6E9",
43
- "0x96244D83DC15d36847C35209bBDc5bdDE9bEc3D8",
44
- "0x09278b36863bE4cCd3d0c22d643E8062D7a11377",
45
- "0x3A21F4E6Bbe527D347ca7c157F4233c935779847",
46
- "0xBC3e5e8C10897a81b63933348f53f2e052F89a7E",
47
- "0x9271EDdda0F0f2bB7b1A0c712bdF8dbD0A38d1Ab",
48
- "0x4C198B3B5F3a4b1Aa706daC73D826c2B795ccd67",
49
- "0x684d4b58Dc32af786BF6D572A792fF7A883428B9",
50
- "0xD3C611AeD139107DEC2294032da3913BC26507fb",
51
- "0x9eF42873Ae015AA3da0c4354AeF94a18D2B3407b",
52
- "0xbdE2Cc5375fa9E0383309A2cA31213f2D6cabcbd",
53
- "0xe69753Ddfbedbd249E703EB374452E78dae1ae49",
54
- "0xE9bc552fdFa54b30296d95F147e3e0280FF7f7e6",
55
- "0xBCA02B395747D62626a65016F2e64A20bd254A39",
56
- "0xF302572594a68aA8F951faE64ED3aE7DA41c72Be",
57
- "0x21032176B43d9f7E9410fB37290a78f4fEd6044C",
58
- "0xD5b58Cf7813c1eDC412367b97876bD400ea5c489",
59
- "0xA5A023E052243b7cce34Cbd4ba20180e8Dea6Ad6",
60
- "0x723a7084028421994d4a7829108D63aB44658315",
61
- "0x1512fcb09463A61862B73ec09B9b354aF1790268",
62
- "0xEB0bAA3A556586192590CAD296b1e48dF62a8549",
63
- "0xf03AfB1c6A11A7E370920ad42e6eE735dBedF0b1",
64
- "0x55CCa2f5eB07907696afe4b9Db5102bcE5feB734",
65
- ].map((a) => a.toLowerCase()));
66
- // Runtime invariant check at module load. Catches a future edit that adds an
67
- // entry via `sanctionedAddresses.add(...)` bypassing the `.map(toLowerCase)` or
68
- // inserts a malformed string. Throwing at load is the loudest signal — the
69
- // package won't import if the dataset is invalid.
70
- for (const entry of sanctionedAddresses) {
71
- if (!/^0x[0-9a-f]{40}$/.test(entry)) {
72
- throw new Error(`sanctioned-addresses.ts: entry "${entry}" is not a well-formed lowercased EVM address. Every entry must match /^0x[0-9a-f]{40}$/ so lowercase lookup matches.`);
73
- }
74
- }
@@ -1,50 +0,0 @@
1
- import type { SimulationLogger, SimulationTransaction, Transfer } from "../types.js";
2
- /**
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.
6
- *
7
- * Addresses are deduped and lowercased; the zero address is removed. Checks run in order:
8
- *
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.
19
- *
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
- * ```
43
- */
44
- export declare function screenAddresses(params: {
45
- simulationTxs: SimulationTransaction[];
46
- transfers: Transfer[];
47
- chainalysisApiKey?: string;
48
- signal?: AbortSignal;
49
- logger?: SimulationLogger;
50
- }): Promise<void>;
@@ -1,168 +0,0 @@
1
- import { getAddress, zeroAddress } from "viem";
2
- import { z } from "zod";
3
- import { AddressScreeningError } from "../errors.js";
4
- import { sanctionedAddresses } from "./sanctioned-addresses.js";
5
- const CHAINALYSIS_API_URL = "https://api.chainalysis.com/api/risk/v2/entities";
6
- const CHAINALYSIS_TIMEOUT_MS = 5_000;
7
- /**
8
- * Known tiers whose presence means NOT severe. Any other value (unknown future
9
- * tier, malformed field) is treated as Severe so a stricter tier Chainalysis
10
- * might introduce later isn't silently downgraded to clean.
11
- */
12
- const NON_SEVERE_TIERS = new Set(["High", "Medium", "Low"]);
13
- const chainalysisEntitySchema = z
14
- .object({
15
- risk: z.string().nullable(),
16
- })
17
- .passthrough();
18
- /**
19
- * Screens every address that appears in a simulated bundle (transfer senders and recipients,
20
- * plus tx `from` / `to`) against the static {@link sanctionedAddresses} list and, when
21
- * configured, the Chainalysis Entity API.
22
- *
23
- * Addresses are deduped and lowercased; the zero address is removed. Checks run in order:
24
- *
25
- * 1. **Static sanctioned list** — always on, no network. Throws on the first hit.
26
- * 2. **Chainalysis Entity API** — one lookup per address, lookups parallelized. Only runs when
27
- * `chainalysisApiKey` is provided.
28
- * - **Fail-open on transport errors before registration** (network / timeout / non-ok POST):
29
- * treat the address as clean and log a warn. A Chainalysis outage must not block all
30
- * simulations.
31
- * - **Fail-closed on partial registration**: if the registration POST succeeded but the
32
- * lookup GET fails, treat the address as Severe.
33
- * - **Fail-closed on schema drift**: an unknown risk tier or unparsed body is treated as
34
- * Severe so a future stricter tier is not silently downgraded.
35
- *
36
- * Intended to be called after {@link simulate}, passing the returned `simulationTxs` and
37
- * `transfers`.
38
- *
39
- * @param params.simulationTxs - The resolved transactions returned by `simulate()`.
40
- * @param params.transfers - The parsed transfers returned by `simulate()`.
41
- * @param params.chainalysisApiKey - Optional Chainalysis token. When absent, only the static
42
- * sanctioned list runs.
43
- * @param params.signal - Optional `AbortSignal` propagated to every Chainalysis request.
44
- * @param params.logger - Optional `SimulationLogger` for warn-level diagnostics.
45
- * @returns Resolves to `void` on a clean screening; throws on any Severe hit.
46
- * @throws {AddressScreeningError} when any address matches the static sanctioned list or
47
- * resolves to Severe via Chainalysis.
48
- * @example
49
- * ```ts
50
- * import { screenAddresses, simulate } from "@morpho-org/evm-simulation";
51
- *
52
- * const result = await simulate(config, params);
53
- * await screenAddresses({
54
- * simulationTxs: result.simulationTxs,
55
- * transfers: result.transfers,
56
- * chainalysisApiKey: process.env.CHAINALYSIS_API_KEY,
57
- * });
58
- * ```
59
- */
60
- export async function screenAddresses(params) {
61
- const { simulationTxs, transfers, chainalysisApiKey, signal, logger } = params;
62
- const addressesToScreen = new Set();
63
- for (const transfer of transfers) {
64
- addressesToScreen.add(transfer.from.toLowerCase());
65
- addressesToScreen.add(transfer.to.toLowerCase());
66
- }
67
- for (const tx of simulationTxs) {
68
- addressesToScreen.add(tx.from.toLowerCase());
69
- addressesToScreen.add(tx.to.toLowerCase());
70
- }
71
- addressesToScreen.delete(zeroAddress);
72
- // 1. Static sanctioned list — always on, no network.
73
- const sanctioned = [];
74
- for (const addr of addressesToScreen) {
75
- if (sanctionedAddresses.has(addr))
76
- sanctioned.push(getAddress(addr));
77
- }
78
- if (sanctioned.length > 0) {
79
- throw new AddressScreeningError("Transaction involves sanctioned addresses", sanctioned);
80
- }
81
- // 2. Chainalysis (optional).
82
- if (!chainalysisApiKey) {
83
- logger?.warn("Chainalysis screening skipped: no API key configured");
84
- return;
85
- }
86
- const results = await Promise.allSettled([...addressesToScreen].map(async (addr) => {
87
- const verdict = await screenWithChainalysis(addr, chainalysisApiKey, signal);
88
- return { addr, verdict };
89
- }));
90
- const flagged = [];
91
- for (const r of results) {
92
- if (r.status === "fulfilled") {
93
- if (r.value.verdict === "severe")
94
- flagged.push(getAddress(r.value.addr));
95
- }
96
- else {
97
- // Transport fail-open: registration POST couldn't even complete, so the
98
- // address was never registered with Chainalysis and the package's
99
- // fail-open posture applies.
100
- logger?.warn("Chainalysis screening failed (fail-open)", {
101
- error: r.reason instanceof Error ? r.reason.message : String(r.reason),
102
- });
103
- }
104
- }
105
- if (flagged.length > 0) {
106
- throw new AddressScreeningError("Transaction involves addresses flagged as Severe risk", flagged);
107
- }
108
- }
109
- /**
110
- * Run the Chainalysis Entity flow for one address.
111
- *
112
- * Returns `'severe'` or `'clean'`. Throws only if the initial registration POST
113
- * fails before any server-side side effect — then the outer handler treats the
114
- * rejection as fail-open. After registration succeeds, every error path returns
115
- * `'severe'` so the check fails closed.
116
- *
117
- * A SINGLE deadline covers both the POST and the GET, keeping the per-address
118
- * wall-time budget at `CHAINALYSIS_TIMEOUT_MS` (5 s) rather than doubling it.
119
- *
120
- * @internal
121
- */
122
- // biome-ignore lint/complexity/useMaxParams: TODO refactor to ≤2 params
123
- async function screenWithChainalysis(address, apiKey, parentSignal) {
124
- const headers = {
125
- "Content-Type": "application/json",
126
- token: apiKey,
127
- };
128
- const deadline = AbortSignal.timeout(CHAINALYSIS_TIMEOUT_MS);
129
- const signal = parentSignal
130
- ? AbortSignal.any([parentSignal, deadline])
131
- : deadline;
132
- // POST to register. Pre-registration failure → fail-open via thrown Error.
133
- const registration = await fetch(CHAINALYSIS_API_URL, {
134
- headers,
135
- method: "POST",
136
- body: JSON.stringify({ address }),
137
- signal,
138
- });
139
- if (!registration.ok) {
140
- throw new Error(`Chainalysis registration failed: ${registration.status} ${registration.statusText}`);
141
- }
142
- // From here on we've already told Chainalysis about the address — fail closed.
143
- let response;
144
- try {
145
- response = await fetch(`${CHAINALYSIS_API_URL}/${encodeURIComponent(address)}`, {
146
- method: "GET",
147
- headers,
148
- signal,
149
- });
150
- }
151
- catch {
152
- return "severe";
153
- }
154
- if (!response.ok)
155
- return "severe";
156
- let parsed;
157
- try {
158
- parsed = chainalysisEntitySchema.parse(await response.json());
159
- }
160
- catch {
161
- return "severe";
162
- }
163
- if (parsed.risk === null)
164
- return "clean";
165
- if (NON_SEVERE_TIERS.has(parsed.risk))
166
- return "clean";
167
- return "severe";
168
- }
@@ -1,20 +0,0 @@
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>;