@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.
- package/README.md +22 -30
- package/lib/cjs/errors.d.ts +0 -6
- package/lib/cjs/errors.js +1 -11
- package/lib/cjs/index.d.ts +2 -3
- package/lib/cjs/index.js +3 -6
- package/lib/cjs/simulate/backends/index.d.ts +1 -1
- package/lib/cjs/simulate/backends/index.js +3 -3
- package/lib/cjs/simulate/backends/tenderly-rpc.d.ts +23 -0
- package/lib/cjs/simulate/backends/tenderly-rpc.js +176 -0
- package/lib/cjs/simulate/parsing/transfers.js +3 -1
- package/lib/cjs/simulate/pipeline/execute-simulation.d.ts +6 -10
- package/lib/cjs/simulate/pipeline/execute-simulation.js +11 -18
- package/lib/cjs/simulate/pipeline/resolve-chain.d.ts +6 -5
- package/lib/cjs/simulate/pipeline/resolve-chain.js +10 -7
- package/lib/cjs/simulate/simulate.d.ts +16 -21
- package/lib/cjs/simulate/simulate.js +16 -18
- package/lib/cjs/types.d.ts +22 -29
- package/lib/esm/errors.d.ts +0 -6
- package/lib/esm/errors.js +0 -9
- package/lib/esm/index.d.ts +2 -3
- package/lib/esm/index.js +1 -2
- package/lib/esm/simulate/backends/index.d.ts +1 -1
- package/lib/esm/simulate/backends/index.js +1 -1
- package/lib/esm/simulate/backends/tenderly-rpc.d.ts +23 -0
- package/lib/esm/simulate/backends/tenderly-rpc.js +173 -0
- package/lib/esm/simulate/parsing/transfers.js +3 -1
- package/lib/esm/simulate/pipeline/execute-simulation.d.ts +6 -10
- package/lib/esm/simulate/pipeline/execute-simulation.js +12 -19
- package/lib/esm/simulate/pipeline/resolve-chain.d.ts +6 -5
- package/lib/esm/simulate/pipeline/resolve-chain.js +10 -7
- package/lib/esm/simulate/simulate.d.ts +16 -21
- package/lib/esm/simulate/simulate.js +16 -18
- package/lib/esm/types.d.ts +22 -29
- package/package.json +3 -3
- package/src/index.ts +1 -3
- package/lib/cjs/screen-addresses/index.d.ts +0 -1
- package/lib/cjs/screen-addresses/index.js +0 -5
- package/lib/cjs/screen-addresses/sanctioned-addresses.d.ts +0 -11
- package/lib/cjs/screen-addresses/sanctioned-addresses.js +0 -77
- package/lib/cjs/screen-addresses/screen-addresses.d.ts +0 -50
- package/lib/cjs/screen-addresses/screen-addresses.js +0 -171
- package/lib/cjs/simulate/backends/tenderly-rest.d.ts +0 -20
- package/lib/cjs/simulate/backends/tenderly-rest.js +0 -279
- package/lib/esm/screen-addresses/index.d.ts +0 -1
- package/lib/esm/screen-addresses/index.js +0 -1
- package/lib/esm/screen-addresses/sanctioned-addresses.d.ts +0 -11
- package/lib/esm/screen-addresses/sanctioned-addresses.js +0 -74
- package/lib/esm/screen-addresses/screen-addresses.d.ts +0 -50
- package/lib/esm/screen-addresses/screen-addresses.js +0 -168
- package/lib/esm/simulate/backends/tenderly-rest.d.ts +0 -20
- 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>;
|