@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,36 +1,34 @@
1
1
  import type { Address, BlockTag, Hex } from "viem";
2
2
  /**
3
- * Credentials and routing for the Tenderly REST simulation backend.
4
- * Omit from `SimulationConfig.tenderlyRest` to disable the Tenderly path entirely.
3
+ * Credentials for the Tenderly Node Web3 Gateway. The `rpcUrl` is the
4
+ * chain-specific gateway URL with the access key embedded in the path,
5
+ * e.g. `https://mainnet.gateway.tenderly.co/{ACCESS_KEY}`.
5
6
  */
6
- export interface TenderlyRestConfig {
7
- /** e.g. "https://api.tenderly.co" */
8
- apiBaseUrl: string;
9
- accessToken: string;
10
- accountSlug: string;
11
- projectSlug: string;
12
- /** Chain IDs that Tenderly supports for simulation */
13
- supportedChainIds: Set<number>;
7
+ export interface TenderlyRpcConfig {
8
+ rpcUrl: string;
14
9
  }
15
10
  /**
16
- * Per-chain capabilities. Stored as a `Map` value in
17
- * `SimulationConfig.chains`, keyed by chain ID.
11
+ * Per-chain backend capabilities. Exactly one configuration shape per chain;
12
+ * the discriminated union enforces that **at least one** of `tenderlyRpc`
13
+ * (primary) or `simulateV1Url` (fallback) is supplied.
18
14
  */
19
- export interface ChainSimulationConfig {
20
- /** JSON-RPC URL supporting `eth_simulateV1`. undefined = not available for this chain. */
15
+ export type ChainSimulationConfig = {
16
+ /** Tenderly RPC config — `tenderly_simulateTransaction` / `tenderly_simulateBundle`. */
17
+ tenderlyRpc: TenderlyRpcConfig;
18
+ /** JSON-RPC URL supporting `eth_simulateV1`. Optional when Tenderly is set. */
21
19
  simulateV1Url?: string;
22
- }
20
+ } | {
21
+ tenderlyRpc?: TenderlyRpcConfig;
22
+ simulateV1Url: string;
23
+ };
23
24
  /**
24
25
  * Top-level configuration for `simulate`.
25
26
  *
26
- * At least one backend must be reachable for a given chainId — either Tenderly
27
- * (via `tenderlyRest` with that chainId in `TenderlyRestConfig.supportedChainIds`)
28
- * or `ChainSimulationConfig.simulateV1Url` in the `chains` map. Otherwise the
29
- * orchestrator throws `UnsupportedChainError`.
27
+ * Every chain entry must declare at least one backend (Tenderly RPC primary,
28
+ * `eth_simulateV1` fallback, or both). Calling `simulate` for a `chainId`
29
+ * missing from `chains` throws `UnsupportedChainError`.
30
30
  */
31
31
  export interface SimulationConfig {
32
- /** Tenderly REST API config. undefined = Tenderly not available. */
33
- tenderlyRest?: TenderlyRestConfig;
34
32
  /** Per-chain simulation capabilities. */
35
33
  chains: Map<number, ChainSimulationConfig>;
36
34
  logger?: SimulationLogger;
@@ -66,6 +64,33 @@ export type SimulationAuthorization = {
66
64
  /** Defaults to maxUint256. Caller can set explicit amount for tokens that don't support max approval. */
67
65
  amount?: bigint;
68
66
  };
67
+ /**
68
+ * Net balance change for a single asset (one token) within an account's entry.
69
+ * Native ETH uses viem's `ethAddress` sentinel as `token`. `symbol`/`decimals`
70
+ * are best-effort and may be absent, notably on the `eth_simulateV1` fallback.
71
+ */
72
+ export interface AssetChange {
73
+ readonly token: Address;
74
+ readonly symbol?: string;
75
+ readonly decimals?: number;
76
+ /** Signed net change of the account's balance, in raw token units. */
77
+ readonly diff: bigint;
78
+ }
79
+ /**
80
+ * Net per-token balance changes for one account over the whole bundle. Returned
81
+ * for every account that nets a non-zero change, the sender and counterparties
82
+ * alike (the zero address is kept for mints/burns). Accounts and their `changes`
83
+ * are sorted by address for deterministic, cross-backend output.
84
+ *
85
+ * Native-ETH coverage differs by backend: Tenderly reports the full net ETH
86
+ * delta (including ETH moved via internal calls), while the `eth_simulateV1`
87
+ * fallback derives native ETH only from the top-level transaction `value`s —
88
+ * ETH moved internally (e.g. a `WETH.withdraw` refund) is not captured there.
89
+ */
90
+ export interface AccountAssetChanges {
91
+ readonly account: Address;
92
+ readonly changes: readonly AssetChange[];
93
+ }
69
94
  /**
70
95
  * A parsed ERC20 / WETH9 transfer extracted from simulation logs. Returned in
71
96
  * `SimulationResult.transfers`.
@@ -89,25 +114,25 @@ export interface Transfer {
89
114
  * - `simulationTxs` is the full resolved transaction list (including
90
115
  * prepended authorization txs).
91
116
  * - `calls[i]` corresponds 1:1 with `simulationTxs[i]` — read raw logs,
92
- * status, returnData/gasUsed, and (Tenderly only) assetChanges per tx.
117
+ * status, returnData/gasUsed.
118
+ * - `assetChanges` is the net per-asset balance change over the whole bundle,
119
+ * grouped by account (sender and counterparties), normalized to the same
120
+ * shape across backends — see `AccountAssetChanges`.
93
121
  * - `transfers[k].txIdx` indexes into `simulationTxs` to attribute each
94
122
  * transfer to its emitting transaction.
95
- * - `tenderlyUrl` is set only when `shareable: true` AND the Tenderly backend
96
- * ran successfully (not the `eth_simulateV1` fallback).
97
123
  */
98
124
  export interface SimulationResult {
99
125
  /** The full resolved transaction list (including prepended authorization txs). */
100
126
  readonly simulationTxs: readonly SimulationTransaction[];
101
127
  /**
102
128
  * Per-transaction normalized output. `calls[i]` corresponds 1:1 with
103
- * `simulationTxs[i]`. Use this to read raw logs, status, return data, gas
104
- * used, and (Tenderly only) asset changes per transaction.
129
+ * `simulationTxs[i]`. Use this to read raw logs, status, return data, gas used.
105
130
  */
106
131
  readonly calls: readonly SimulationCall[];
107
132
  /** Parsed ERC-20 / WETH9 transfers from the simulation. */
108
133
  readonly transfers: readonly Transfer[];
109
- /** Shareable Tenderly URL. Present only when `shareable: true` and Tenderly ran (not fallback). */
110
- readonly tenderlyUrl?: string;
134
+ /** Net per-asset balance changes, grouped by account, over the whole bundle. */
135
+ readonly assetChanges: readonly AccountAssetChanges[];
111
136
  }
112
137
  /** Minimal structured logger the package calls for warnings and info. */
113
138
  export interface SimulationLogger {
@@ -128,15 +153,15 @@ export interface SimulateParams {
128
153
  /**
129
154
  * Internal raw result from a simulation adapter before normalization.
130
155
  * `calls[i]` corresponds 1:1 with the i-th transaction passed to the
131
- * backend.
156
+ * backend; `assetChanges` is the bundle-level aggregate grouped by account.
132
157
  */
133
158
  export interface RawSimulationResult {
134
159
  calls: RawCall[];
135
- tenderlyUrl?: string;
160
+ assetChanges: AccountAssetChanges[];
136
161
  }
137
162
  /**
138
163
  * Normalized EVM log emitted by a single simulated call. The shape is the
139
- * common subset both backends (`eth_simulateV1` via viem and Tenderly REST)
164
+ * common subset both backends (`eth_simulateV1` via viem and Tenderly RPC)
140
165
  * produce after schema validation. Returned indirectly via
141
166
  * `SimulationCall.logs` and consumed by the SDK's transfer parser.
142
167
  */
@@ -156,15 +181,14 @@ export interface RawCall {
156
181
  status: boolean;
157
182
  returnData: Hex;
158
183
  gasUsed: bigint;
159
- /** Tenderly-only `asset_changes` payload. Absent on `eth_simulateV1`. */
160
- assetChanges?: unknown;
161
184
  }
162
185
  /**
163
186
  * Per-transaction normalized output from the simulation backend.
164
187
  *
165
188
  * `SimulationResult.calls[i]` corresponds 1:1 with
166
189
  * `SimulationResult.simulationTxs[i]`. Use this to read raw logs, status,
167
- * return data, gas used, and (Tenderly only) asset changes per transaction.
190
+ * return data, and gas used. Net asset changes are reported at the bundle
191
+ * level — see `SimulationResult.assetChanges`.
168
192
  */
169
193
  export interface SimulationCall {
170
194
  readonly logs: readonly RawLog[];
@@ -179,9 +203,4 @@ export interface SimulationCall {
179
203
  readonly returnData: Hex;
180
204
  /** Gas used by this call (root frame). */
181
205
  readonly gasUsed: bigint;
182
- /**
183
- * Tenderly-only `asset_changes` payload for this tx. Opaque `unknown` —
184
- * do not destructure without validation. Absent on `eth_simulateV1`.
185
- */
186
- readonly assetChanges?: unknown;
187
206
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@morpho-org/evm-simulation",
3
3
  "description": "EVM simulation engine for Morpho transactions.",
4
- "version": "2.0.2",
4
+ "version": "4.0.0",
5
5
  "author": "Morpho Association <contact@morpho.org>",
6
6
  "contributors": [
7
7
  "Rubilmax <rmilon@gmail.com>"
@@ -25,8 +25,8 @@
25
25
  ],
26
26
  "dependencies": {
27
27
  "zod": "^4.4.3",
28
- "@morpho-org/blue-sdk": "^6.0.1",
29
- "@morpho-org/morpho-ts": "^2.5.3"
28
+ "@morpho-org/blue-sdk": "^6.1.0",
29
+ "@morpho-org/morpho-ts": "^2.6.0"
30
30
  },
31
31
  "peerDependencies": {
32
32
  "viem": "^2.0.0"
@@ -34,14 +34,16 @@
34
34
  "devDependencies": {
35
35
  "typescript": "^6.0.3",
36
36
  "viem": "^2.50.4",
37
- "vitest": "^4.1.7"
37
+ "vitest": "^4.1.7",
38
+ "@morpho-org/test": "^2.8.1"
38
39
  },
39
40
  "scripts": {
40
41
  "prepublish": "$npm_execpath build",
41
42
  "build": "tsc --noEmit && $npm_execpath build:cjs && $npm_execpath build:esm",
42
43
  "build:cjs": "tsc --build tsconfig.build.cjs.json && echo '{\"type\":\"commonjs\"}' > lib/cjs/package.json",
43
44
  "build:esm": "tsc --build tsconfig.build.esm.json && echo '{\"type\":\"module\"}' > lib/esm/package.json",
44
- "test": "vitest --root ../.. --project evm-simulation"
45
+ "test": "vitest --root ../.. --project evm-simulation",
46
+ "test:fork": "dotenv -- vitest --root ../.. --project evm-simulation-fork"
45
47
  },
46
48
  "exports": {
47
49
  ".": {
package/src/index.ts CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  // Errors (for instanceof checks by consumers)
4
4
  export {
5
- AddressScreeningError,
6
5
  BlacklistViolationError,
7
6
  ExternalServiceError,
8
7
  SimulationPackageError,
@@ -10,10 +9,11 @@ export {
10
9
  SimulationValidationError,
11
10
  UnsupportedChainError,
12
11
  } from "./errors.js";
13
- export { screenAddresses } from "./screen-addresses/index.js";
14
12
  export { simulate } from "./simulate/index.js";
15
13
  // Types
16
14
  export type {
15
+ AccountAssetChanges,
16
+ AssetChange,
17
17
  ChainSimulationConfig,
18
18
  RawLog,
19
19
  SimulateParams,
@@ -23,6 +23,6 @@ export type {
23
23
  SimulationLogger,
24
24
  SimulationResult,
25
25
  SimulationTransaction,
26
- TenderlyRestConfig,
26
+ TenderlyRpcConfig,
27
27
  Transfer,
28
28
  } from "./types.js";
@@ -1 +0,0 @@
1
- export { screenAddresses } from "./screen-addresses.js";
@@ -1,5 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.screenAddresses = void 0;
4
- var screen_addresses_js_1 = require("./screen-addresses.js");
5
- Object.defineProperty(exports, "screenAddresses", { enumerable: true, get: function () { return screen_addresses_js_1.screenAddresses; } });
@@ -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,77 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.sanctionedAddresses = void 0;
4
- /**
5
- * Static list of sanctioned/compromised addresses.
6
- * Ported from offchain-services-server/src/functions/screen/blacklist.ts.
7
- * These addresses are checked before calling the Chainalysis API (fast, no network).
8
- *
9
- * **Invariant** (enforced at module load, see below): every entry is a
10
- * well-formed lowercased EVM address. `screenAddresses` does
11
- * `sanctionedAddresses.has(addr.toLowerCase())` — a mixed-case entry would
12
- * never match and would silently bypass the sanctioned check.
13
- */
14
- exports.sanctionedAddresses = new Set([
15
- // Bybit hack addresses
16
- "0x47666Fab8bd0Ac7003bce3f5C3585383F09486E2",
17
- "0xA4B2Fd68593B6F34E51cB9eDB66E71c1B4Ab449e",
18
- "0x36ed3C0213565530C35115d93A80F9c04d94E4Cb",
19
- "0x1542368a03ad1f03d96D51B414f4738961Cf4443",
20
- "0xdD90071D52F20e85c89802e5Dc1eC0A7B6475f92",
21
- "0x40e98FeEEbaD7Ddb0F0534Ccaa617427eA10187e",
22
- "0x30a822CDD2782D2B2A12a08526452e885978FA1D",
23
- "0x6d46bd3AfF100f23C194e5312f93507978a6DC91",
24
- "0x660BfcEa3A5FAF823e8f8bF57dd558db034dea1d",
25
- "0x140c9Ab92347734641b1A7c124ffDeE58c20C3E3",
26
- "0x8c7235e1A6EeF91b980D0FcA083347FBb7EE1806",
27
- "0xb172F7e99452446f18FF49A71bfEeCf0873003b4",
28
- "0xCd7eC020121Ead6f99855cbB972dF502dB5bC63a",
29
- "0x0e8C1E2881F35Ef20343264862A242FB749d6b35",
30
- "0x2290937A4498C96eFfb87b8371a33D108F8D433f",
31
- "0x1bb0970508316DC735329752a4581E0a4bAbc6B4",
32
- "0xf0a16603289eAF35F64077Ba3681af41194a1c09",
33
- "0x5Af75eAB6BEC227657fA3E749a8BFd55f02e4b1D",
34
- "0xB4a862A81aBB2f952FcA4C6f5510962e18c7f1A2",
35
- "0x959c4CA19c4532C97A657D82d97acCBAb70e6fb4",
36
- "0xB72334cB9D0b614D30C4c60e2bd12fF5Ed03c305",
37
- "0xFc926659Dd8808f6e3e0a8d61B20B871F3Fa6465",
38
- "0xfa3FcCCB897079fD83bfBA690E7D47Eb402d6c49",
39
- "0x51E9d833Ecae4E8D9D8Be17300AEE6D3398C135D",
40
- "0x1eB27f136BFe7947f80d6ceE3Cf0bfDf92b45e57",
41
- "0x83Ef5E80faD88288F770152875Ab0bb16641a09E",
42
- "0x52207Ec7B1b43AA5DB116931a904371ae2C1619e",
43
- "0x23Db729908137cb60852f2936D2b5c6De0e1c887",
44
- "0xAF620E6d32B1c67f3396EF5d2F7d7642Dc2e6CE9",
45
- "0xCd1a4A457cA8b0931c3BF81Df3CFa227ADBdb6E9",
46
- "0x96244D83DC15d36847C35209bBDc5bdDE9bEc3D8",
47
- "0x09278b36863bE4cCd3d0c22d643E8062D7a11377",
48
- "0x3A21F4E6Bbe527D347ca7c157F4233c935779847",
49
- "0xBC3e5e8C10897a81b63933348f53f2e052F89a7E",
50
- "0x9271EDdda0F0f2bB7b1A0c712bdF8dbD0A38d1Ab",
51
- "0x4C198B3B5F3a4b1Aa706daC73D826c2B795ccd67",
52
- "0x684d4b58Dc32af786BF6D572A792fF7A883428B9",
53
- "0xD3C611AeD139107DEC2294032da3913BC26507fb",
54
- "0x9eF42873Ae015AA3da0c4354AeF94a18D2B3407b",
55
- "0xbdE2Cc5375fa9E0383309A2cA31213f2D6cabcbd",
56
- "0xe69753Ddfbedbd249E703EB374452E78dae1ae49",
57
- "0xE9bc552fdFa54b30296d95F147e3e0280FF7f7e6",
58
- "0xBCA02B395747D62626a65016F2e64A20bd254A39",
59
- "0xF302572594a68aA8F951faE64ED3aE7DA41c72Be",
60
- "0x21032176B43d9f7E9410fB37290a78f4fEd6044C",
61
- "0xD5b58Cf7813c1eDC412367b97876bD400ea5c489",
62
- "0xA5A023E052243b7cce34Cbd4ba20180e8Dea6Ad6",
63
- "0x723a7084028421994d4a7829108D63aB44658315",
64
- "0x1512fcb09463A61862B73ec09B9b354aF1790268",
65
- "0xEB0bAA3A556586192590CAD296b1e48dF62a8549",
66
- "0xf03AfB1c6A11A7E370920ad42e6eE735dBedF0b1",
67
- "0x55CCa2f5eB07907696afe4b9Db5102bcE5feB734",
68
- ].map((a) => a.toLowerCase()));
69
- // Runtime invariant check at module load. Catches a future edit that adds an
70
- // entry via `sanctionedAddresses.add(...)` bypassing the `.map(toLowerCase)` or
71
- // inserts a malformed string. Throwing at load is the loudest signal — the
72
- // package won't import if the dataset is invalid.
73
- for (const entry of exports.sanctionedAddresses) {
74
- if (!/^0x[0-9a-f]{40}$/.test(entry)) {
75
- 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.`);
76
- }
77
- }
@@ -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,171 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.screenAddresses = screenAddresses;
4
- const viem_1 = require("viem");
5
- const zod_1 = require("zod");
6
- const errors_js_1 = require("../errors.js");
7
- const sanctioned_addresses_js_1 = require("./sanctioned-addresses.js");
8
- const CHAINALYSIS_API_URL = "https://api.chainalysis.com/api/risk/v2/entities";
9
- const CHAINALYSIS_TIMEOUT_MS = 5_000;
10
- /**
11
- * Known tiers whose presence means NOT severe. Any other value (unknown future
12
- * tier, malformed field) is treated as Severe so a stricter tier Chainalysis
13
- * might introduce later isn't silently downgraded to clean.
14
- */
15
- const NON_SEVERE_TIERS = new Set(["High", "Medium", "Low"]);
16
- const chainalysisEntitySchema = zod_1.z
17
- .object({
18
- risk: zod_1.z.string().nullable(),
19
- })
20
- .passthrough();
21
- /**
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.
25
- *
26
- * Addresses are deduped and lowercased; the zero address is removed. Checks run in order:
27
- *
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.
38
- *
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
- * ```
62
- */
63
- async function screenAddresses(params) {
64
- const { simulationTxs, transfers, chainalysisApiKey, signal, logger } = params;
65
- const addressesToScreen = new Set();
66
- for (const transfer of transfers) {
67
- addressesToScreen.add(transfer.from.toLowerCase());
68
- addressesToScreen.add(transfer.to.toLowerCase());
69
- }
70
- for (const tx of simulationTxs) {
71
- addressesToScreen.add(tx.from.toLowerCase());
72
- addressesToScreen.add(tx.to.toLowerCase());
73
- }
74
- addressesToScreen.delete(viem_1.zeroAddress);
75
- // 1. Static sanctioned list — always on, no network.
76
- const sanctioned = [];
77
- for (const addr of addressesToScreen) {
78
- if (sanctioned_addresses_js_1.sanctionedAddresses.has(addr))
79
- sanctioned.push((0, viem_1.getAddress)(addr));
80
- }
81
- if (sanctioned.length > 0) {
82
- throw new errors_js_1.AddressScreeningError("Transaction involves sanctioned addresses", sanctioned);
83
- }
84
- // 2. Chainalysis (optional).
85
- if (!chainalysisApiKey) {
86
- logger?.warn("Chainalysis screening skipped: no API key configured");
87
- return;
88
- }
89
- const results = await Promise.allSettled([...addressesToScreen].map(async (addr) => {
90
- const verdict = await screenWithChainalysis(addr, chainalysisApiKey, signal);
91
- return { addr, verdict };
92
- }));
93
- const flagged = [];
94
- for (const r of results) {
95
- if (r.status === "fulfilled") {
96
- if (r.value.verdict === "severe")
97
- flagged.push((0, viem_1.getAddress)(r.value.addr));
98
- }
99
- else {
100
- // Transport fail-open: registration POST couldn't even complete, so the
101
- // address was never registered with Chainalysis and the package's
102
- // fail-open posture applies.
103
- logger?.warn("Chainalysis screening failed (fail-open)", {
104
- error: r.reason instanceof Error ? r.reason.message : String(r.reason),
105
- });
106
- }
107
- }
108
- if (flagged.length > 0) {
109
- throw new errors_js_1.AddressScreeningError("Transaction involves addresses flagged as Severe risk", flagged);
110
- }
111
- }
112
- /**
113
- * Run the Chainalysis Entity flow for one address.
114
- *
115
- * Returns `'severe'` or `'clean'`. Throws only if the initial registration POST
116
- * fails before any server-side side effect — then the outer handler treats the
117
- * rejection as fail-open. After registration succeeds, every error path returns
118
- * `'severe'` so the check fails closed.
119
- *
120
- * A SINGLE deadline covers both the POST and the GET, keeping the per-address
121
- * wall-time budget at `CHAINALYSIS_TIMEOUT_MS` (5 s) rather than doubling it.
122
- *
123
- * @internal
124
- */
125
- // biome-ignore lint/complexity/useMaxParams: TODO refactor to ≤2 params
126
- async function screenWithChainalysis(address, apiKey, parentSignal) {
127
- const headers = {
128
- "Content-Type": "application/json",
129
- token: apiKey,
130
- };
131
- const deadline = AbortSignal.timeout(CHAINALYSIS_TIMEOUT_MS);
132
- const signal = parentSignal
133
- ? AbortSignal.any([parentSignal, deadline])
134
- : deadline;
135
- // POST to register. Pre-registration failure → fail-open via thrown Error.
136
- const registration = await fetch(CHAINALYSIS_API_URL, {
137
- headers,
138
- method: "POST",
139
- body: JSON.stringify({ address }),
140
- signal,
141
- });
142
- if (!registration.ok) {
143
- throw new Error(`Chainalysis registration failed: ${registration.status} ${registration.statusText}`);
144
- }
145
- // From here on we've already told Chainalysis about the address — fail closed.
146
- let response;
147
- try {
148
- response = await fetch(`${CHAINALYSIS_API_URL}/${encodeURIComponent(address)}`, {
149
- method: "GET",
150
- headers,
151
- signal,
152
- });
153
- }
154
- catch {
155
- return "severe";
156
- }
157
- if (!response.ok)
158
- return "severe";
159
- let parsed;
160
- try {
161
- parsed = chainalysisEntitySchema.parse(await response.json());
162
- }
163
- catch {
164
- return "severe";
165
- }
166
- if (parsed.risk === null)
167
- return "clean";
168
- if (NON_SEVERE_TIERS.has(parsed.risk))
169
- return "clean";
170
- return "severe";
171
- }
@@ -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>;