@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.
- package/LICENSE +21 -0
- package/README.md +77 -0
- package/lib/cjs/errors.d.ts +50 -0
- package/lib/cjs/errors.js +72 -0
- package/lib/cjs/index.d.ts +4 -0
- package/lib/cjs/index.js +17 -0
- package/lib/cjs/package.json +1 -0
- package/lib/cjs/screen-addresses/index.d.ts +1 -0
- package/lib/cjs/screen-addresses/index.js +5 -0
- package/lib/cjs/screen-addresses/sanctioned-addresses.d.ts +11 -0
- package/lib/cjs/screen-addresses/sanctioned-addresses.js +77 -0
- package/lib/cjs/screen-addresses/screen-addresses.d.ts +34 -0
- package/lib/cjs/screen-addresses/screen-addresses.js +152 -0
- package/lib/cjs/simulate/authorizations/index.d.ts +2 -0
- package/lib/cjs/simulate/authorizations/index.js +7 -0
- package/lib/cjs/simulate/authorizations/resolve.d.ts +9 -0
- package/lib/cjs/simulate/authorizations/resolve.js +29 -0
- package/lib/cjs/simulate/authorizations/validate.d.ts +6 -0
- package/lib/cjs/simulate/authorizations/validate.js +23 -0
- package/lib/cjs/simulate/backends/eth-simulate-v1.d.ts +15 -0
- package/lib/cjs/simulate/backends/eth-simulate-v1.js +76 -0
- package/lib/cjs/simulate/backends/index.d.ts +2 -0
- package/lib/cjs/simulate/backends/index.js +7 -0
- package/lib/cjs/simulate/backends/tenderly-rest.d.ts +20 -0
- package/lib/cjs/simulate/backends/tenderly-rest.js +269 -0
- package/lib/cjs/simulate/index.d.ts +1 -0
- package/lib/cjs/simulate/index.js +5 -0
- package/lib/cjs/simulate/parsing/index.d.ts +1 -0
- package/lib/cjs/simulate/parsing/index.js +5 -0
- package/lib/cjs/simulate/parsing/transfers.d.ts +27 -0
- package/lib/cjs/simulate/parsing/transfers.js +153 -0
- package/lib/cjs/simulate/pipeline/build-simulation-txs.d.ts +17 -0
- package/lib/cjs/simulate/pipeline/build-simulation-txs.js +50 -0
- package/lib/cjs/simulate/pipeline/bundler-retention.d.ts +19 -0
- package/lib/cjs/simulate/pipeline/bundler-retention.js +90 -0
- package/lib/cjs/simulate/pipeline/execute-simulation.d.ts +25 -0
- package/lib/cjs/simulate/pipeline/execute-simulation.js +87 -0
- package/lib/cjs/simulate/pipeline/index.d.ts +4 -0
- package/lib/cjs/simulate/pipeline/index.js +11 -0
- package/lib/cjs/simulate/pipeline/resolve-chain.d.ts +14 -0
- package/lib/cjs/simulate/pipeline/resolve-chain.js +19 -0
- package/lib/cjs/simulate/pipeline/validate-input.d.ts +10 -0
- package/lib/cjs/simulate/pipeline/validate-input.js +55 -0
- package/lib/cjs/simulate/simulate.d.ts +26 -0
- package/lib/cjs/simulate/simulate.js +46 -0
- package/lib/cjs/types.d.ts +131 -0
- package/lib/cjs/types.js +2 -0
- package/lib/esm/errors.d.ts +50 -0
- package/lib/esm/errors.js +62 -0
- package/lib/esm/index.d.ts +4 -0
- package/lib/esm/index.js +5 -0
- package/lib/esm/package.json +1 -0
- package/lib/esm/screen-addresses/index.d.ts +1 -0
- package/lib/esm/screen-addresses/index.js +1 -0
- package/lib/esm/screen-addresses/sanctioned-addresses.d.ts +11 -0
- package/lib/esm/screen-addresses/sanctioned-addresses.js +74 -0
- package/lib/esm/screen-addresses/screen-addresses.d.ts +34 -0
- package/lib/esm/screen-addresses/screen-addresses.js +149 -0
- package/lib/esm/simulate/authorizations/index.d.ts +2 -0
- package/lib/esm/simulate/authorizations/index.js +2 -0
- package/lib/esm/simulate/authorizations/resolve.d.ts +9 -0
- package/lib/esm/simulate/authorizations/resolve.js +26 -0
- package/lib/esm/simulate/authorizations/validate.d.ts +6 -0
- package/lib/esm/simulate/authorizations/validate.js +20 -0
- package/lib/esm/simulate/backends/eth-simulate-v1.d.ts +15 -0
- package/lib/esm/simulate/backends/eth-simulate-v1.js +73 -0
- package/lib/esm/simulate/backends/index.d.ts +2 -0
- package/lib/esm/simulate/backends/index.js +2 -0
- package/lib/esm/simulate/backends/tenderly-rest.d.ts +20 -0
- package/lib/esm/simulate/backends/tenderly-rest.js +266 -0
- package/lib/esm/simulate/index.d.ts +1 -0
- package/lib/esm/simulate/index.js +1 -0
- package/lib/esm/simulate/parsing/index.d.ts +1 -0
- package/lib/esm/simulate/parsing/index.js +1 -0
- package/lib/esm/simulate/parsing/transfers.d.ts +27 -0
- package/lib/esm/simulate/parsing/transfers.js +149 -0
- package/lib/esm/simulate/pipeline/build-simulation-txs.d.ts +17 -0
- package/lib/esm/simulate/pipeline/build-simulation-txs.js +47 -0
- package/lib/esm/simulate/pipeline/bundler-retention.d.ts +19 -0
- package/lib/esm/simulate/pipeline/bundler-retention.js +87 -0
- package/lib/esm/simulate/pipeline/execute-simulation.d.ts +25 -0
- package/lib/esm/simulate/pipeline/execute-simulation.js +84 -0
- package/lib/esm/simulate/pipeline/index.d.ts +4 -0
- package/lib/esm/simulate/pipeline/index.js +4 -0
- package/lib/esm/simulate/pipeline/resolve-chain.d.ts +14 -0
- package/lib/esm/simulate/pipeline/resolve-chain.js +16 -0
- package/lib/esm/simulate/pipeline/validate-input.d.ts +10 -0
- package/lib/esm/simulate/pipeline/validate-input.js +52 -0
- package/lib/esm/simulate/simulate.d.ts +26 -0
- package/lib/esm/simulate/simulate.js +43 -0
- package/lib/esm/types.d.ts +131 -0
- package/lib/esm/types.js +1 -0
- package/package.json +49 -0
- package/src/index.ts +27 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { screenAddresses } from "./screen-addresses.js";
|
|
@@ -0,0 +1,11 @@
|
|
|
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>;
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { SimulationLogger, SimulationTransaction, Transfer } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Screen all relevant addresses from a simulation for sanctioned/compromised addresses.
|
|
4
|
+
*
|
|
5
|
+
* Screening targets (deduped, lowercased, zero-addr removed):
|
|
6
|
+
* - Transfer senders and recipients from simulation result
|
|
7
|
+
* - `from` and `to` of every tx in `simulationTxs` (catches native-value flows)
|
|
8
|
+
*
|
|
9
|
+
* Checks run in order:
|
|
10
|
+
* 1. Static {@link sanctionedAddresses} set (fast, no network, always on).
|
|
11
|
+
* 2. Chainalysis Entity API — one lookup per address, lookups across addresses
|
|
12
|
+
* run in parallel. Only fires if `chainalysisApiKey` is provided.
|
|
13
|
+
* **Fail-open on transport errors before registration** (network / timeout /
|
|
14
|
+
* non-ok POST): the address is treated as clean, a warn is logged, and the
|
|
15
|
+
* simulation proceeds. This is the documented posture — a Chainalysis
|
|
16
|
+
* outage must not block all simulations.
|
|
17
|
+
* **Fail-closed on partial registration**: if the registration POST succeeded
|
|
18
|
+
* but the subsequent lookup GET fails, the address is treated as Severe.
|
|
19
|
+
* The data already persisted server-side, so failing open at this point
|
|
20
|
+
* would let a pass slip through based on a half-completed check.
|
|
21
|
+
* **Fail-closed on schema drift**: if Chainalysis returns an unknown risk
|
|
22
|
+
* tier or a body that doesn't parse, the address is treated as Severe so a
|
|
23
|
+
* future stricter tier isn't silently downgraded.
|
|
24
|
+
*
|
|
25
|
+
* Throws `AddressScreeningError` on any Severe hit. Intended to be called after
|
|
26
|
+
* `simulate`, passing the returned `simulationTxs` and `transfers`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function screenAddresses(params: {
|
|
29
|
+
simulationTxs: SimulationTransaction[];
|
|
30
|
+
transfers: Transfer[];
|
|
31
|
+
chainalysisApiKey?: string;
|
|
32
|
+
signal?: AbortSignal;
|
|
33
|
+
logger?: SimulationLogger;
|
|
34
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,149 @@
|
|
|
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
|
+
* Screen all relevant addresses from a simulation for sanctioned/compromised addresses.
|
|
20
|
+
*
|
|
21
|
+
* Screening targets (deduped, lowercased, zero-addr removed):
|
|
22
|
+
* - Transfer senders and recipients from simulation result
|
|
23
|
+
* - `from` and `to` of every tx in `simulationTxs` (catches native-value flows)
|
|
24
|
+
*
|
|
25
|
+
* Checks run in order:
|
|
26
|
+
* 1. Static {@link sanctionedAddresses} set (fast, no network, always on).
|
|
27
|
+
* 2. Chainalysis Entity API — one lookup per address, lookups across addresses
|
|
28
|
+
* run in parallel. Only fires if `chainalysisApiKey` is provided.
|
|
29
|
+
* **Fail-open on transport errors before registration** (network / timeout /
|
|
30
|
+
* non-ok POST): the address is treated as clean, a warn is logged, and the
|
|
31
|
+
* simulation proceeds. This is the documented posture — a Chainalysis
|
|
32
|
+
* outage must not block all simulations.
|
|
33
|
+
* **Fail-closed on partial registration**: if the registration POST succeeded
|
|
34
|
+
* but the subsequent lookup GET fails, the address is treated as Severe.
|
|
35
|
+
* The data already persisted server-side, so failing open at this point
|
|
36
|
+
* would let a pass slip through based on a half-completed check.
|
|
37
|
+
* **Fail-closed on schema drift**: if Chainalysis returns an unknown risk
|
|
38
|
+
* tier or a body that doesn't parse, the address is treated as Severe so a
|
|
39
|
+
* future stricter tier isn't silently downgraded.
|
|
40
|
+
*
|
|
41
|
+
* Throws `AddressScreeningError` on any Severe hit. Intended to be called after
|
|
42
|
+
* `simulate`, passing the returned `simulationTxs` and `transfers`.
|
|
43
|
+
*/
|
|
44
|
+
export async function screenAddresses(params) {
|
|
45
|
+
const { simulationTxs, transfers, chainalysisApiKey, signal, logger } = params;
|
|
46
|
+
const addressesToScreen = new Set();
|
|
47
|
+
for (const transfer of transfers) {
|
|
48
|
+
addressesToScreen.add(transfer.from.toLowerCase());
|
|
49
|
+
addressesToScreen.add(transfer.to.toLowerCase());
|
|
50
|
+
}
|
|
51
|
+
for (const tx of simulationTxs) {
|
|
52
|
+
addressesToScreen.add(tx.from.toLowerCase());
|
|
53
|
+
addressesToScreen.add(tx.to.toLowerCase());
|
|
54
|
+
}
|
|
55
|
+
addressesToScreen.delete(zeroAddress);
|
|
56
|
+
// 1. Static sanctioned list — always on, no network.
|
|
57
|
+
const sanctioned = [];
|
|
58
|
+
for (const addr of addressesToScreen) {
|
|
59
|
+
if (sanctionedAddresses.has(addr))
|
|
60
|
+
sanctioned.push(getAddress(addr));
|
|
61
|
+
}
|
|
62
|
+
if (sanctioned.length > 0) {
|
|
63
|
+
throw new AddressScreeningError("Transaction involves sanctioned addresses", sanctioned);
|
|
64
|
+
}
|
|
65
|
+
// 2. Chainalysis (optional).
|
|
66
|
+
if (!chainalysisApiKey) {
|
|
67
|
+
logger?.warn("Chainalysis screening skipped: no API key configured");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const results = await Promise.allSettled([...addressesToScreen].map(async (addr) => {
|
|
71
|
+
const verdict = await screenWithChainalysis(addr, chainalysisApiKey, signal);
|
|
72
|
+
return { addr, verdict };
|
|
73
|
+
}));
|
|
74
|
+
const flagged = [];
|
|
75
|
+
for (const r of results) {
|
|
76
|
+
if (r.status === "fulfilled") {
|
|
77
|
+
if (r.value.verdict === "severe")
|
|
78
|
+
flagged.push(getAddress(r.value.addr));
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
// Transport fail-open: registration POST couldn't even complete, so the
|
|
82
|
+
// address was never registered with Chainalysis and the package's
|
|
83
|
+
// fail-open posture applies.
|
|
84
|
+
logger?.warn("Chainalysis screening failed (fail-open)", {
|
|
85
|
+
error: r.reason instanceof Error ? r.reason.message : String(r.reason),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (flagged.length > 0) {
|
|
90
|
+
throw new AddressScreeningError("Transaction involves addresses flagged as Severe risk", flagged);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Run the Chainalysis Entity flow for one address.
|
|
95
|
+
*
|
|
96
|
+
* Returns `'severe'` or `'clean'`. Throws only if the initial registration POST
|
|
97
|
+
* fails before any server-side side effect — then the outer handler treats the
|
|
98
|
+
* rejection as fail-open. After registration succeeds, every error path returns
|
|
99
|
+
* `'severe'` so the check fails closed.
|
|
100
|
+
*
|
|
101
|
+
* A SINGLE deadline covers both the POST and the GET, keeping the per-address
|
|
102
|
+
* wall-time budget at `CHAINALYSIS_TIMEOUT_MS` (5 s) rather than doubling it.
|
|
103
|
+
*/
|
|
104
|
+
async function screenWithChainalysis(address, apiKey, parentSignal) {
|
|
105
|
+
const headers = {
|
|
106
|
+
"Content-Type": "application/json",
|
|
107
|
+
token: apiKey,
|
|
108
|
+
};
|
|
109
|
+
const deadline = AbortSignal.timeout(CHAINALYSIS_TIMEOUT_MS);
|
|
110
|
+
const signal = parentSignal
|
|
111
|
+
? AbortSignal.any([parentSignal, deadline])
|
|
112
|
+
: deadline;
|
|
113
|
+
// POST to register. Pre-registration failure → fail-open via thrown Error.
|
|
114
|
+
const registration = await fetch(CHAINALYSIS_API_URL, {
|
|
115
|
+
headers,
|
|
116
|
+
method: "POST",
|
|
117
|
+
body: JSON.stringify({ address }),
|
|
118
|
+
signal,
|
|
119
|
+
});
|
|
120
|
+
if (!registration.ok) {
|
|
121
|
+
throw new Error(`Chainalysis registration failed: ${registration.status} ${registration.statusText}`);
|
|
122
|
+
}
|
|
123
|
+
// From here on we've already told Chainalysis about the address — fail closed.
|
|
124
|
+
let response;
|
|
125
|
+
try {
|
|
126
|
+
response = await fetch(`${CHAINALYSIS_API_URL}/${encodeURIComponent(address)}`, {
|
|
127
|
+
method: "GET",
|
|
128
|
+
headers,
|
|
129
|
+
signal,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return "severe";
|
|
134
|
+
}
|
|
135
|
+
if (!response.ok)
|
|
136
|
+
return "severe";
|
|
137
|
+
let parsed;
|
|
138
|
+
try {
|
|
139
|
+
parsed = chainalysisEntitySchema.parse(await response.json());
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return "severe";
|
|
143
|
+
}
|
|
144
|
+
if (parsed.risk === null)
|
|
145
|
+
return "clean";
|
|
146
|
+
if (NON_SEVERE_TIERS.has(parsed.risk))
|
|
147
|
+
return "clean";
|
|
148
|
+
return "severe";
|
|
149
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type Address } from "viem";
|
|
2
|
+
import type { SimulationAuthorization, SimulationTransaction } from "../../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Resolve SimulationAuthorization[] into SimulationTransaction[].
|
|
5
|
+
*
|
|
6
|
+
* - "approval" → use authorization.transaction as-is
|
|
7
|
+
* - "signature" → encode approve(spender, amount) on the token contract
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolveAuthorizations(authorizations: SimulationAuthorization[], sender: Address): SimulationTransaction[];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { encodeFunctionData, erc20Abi, maxUint256 } from "viem";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve SimulationAuthorization[] into SimulationTransaction[].
|
|
4
|
+
*
|
|
5
|
+
* - "approval" → use authorization.transaction as-is
|
|
6
|
+
* - "signature" → encode approve(spender, amount) on the token contract
|
|
7
|
+
*/
|
|
8
|
+
export function resolveAuthorizations(authorizations, sender) {
|
|
9
|
+
return authorizations.map((auth) => {
|
|
10
|
+
if (auth.type === "approval") {
|
|
11
|
+
return auth.transaction;
|
|
12
|
+
}
|
|
13
|
+
const amount = auth.amount ?? maxUint256;
|
|
14
|
+
const data = encodeFunctionData({
|
|
15
|
+
abi: erc20Abi,
|
|
16
|
+
functionName: "approve",
|
|
17
|
+
args: [auth.spender, amount],
|
|
18
|
+
});
|
|
19
|
+
return {
|
|
20
|
+
from: sender,
|
|
21
|
+
to: auth.token,
|
|
22
|
+
data,
|
|
23
|
+
value: 0n,
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { SimulationAuthorization } from "../../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Validate all signature authorizations have non-zero token and spender.
|
|
4
|
+
* Returns an array of error strings (empty if valid).
|
|
5
|
+
*/
|
|
6
|
+
export declare function validateAuthorizations(authorizations: SimulationAuthorization[]): string[];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { zeroAddress } from "viem";
|
|
2
|
+
/**
|
|
3
|
+
* Validate all signature authorizations have non-zero token and spender.
|
|
4
|
+
* Returns an array of error strings (empty if valid).
|
|
5
|
+
*/
|
|
6
|
+
export function validateAuthorizations(authorizations) {
|
|
7
|
+
const errors = [];
|
|
8
|
+
for (let i = 0; i < authorizations.length; i++) {
|
|
9
|
+
const auth = authorizations[i];
|
|
10
|
+
if (auth.type === "signature") {
|
|
11
|
+
if (!auth.token || auth.token === zeroAddress) {
|
|
12
|
+
errors.push(`authorizations[${i}]: signature authorization has zero-address token`);
|
|
13
|
+
}
|
|
14
|
+
if (!auth.spender || auth.spender === zeroAddress) {
|
|
15
|
+
errors.push(`authorizations[${i}]: signature authorization has zero-address spender`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return errors;
|
|
20
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type BlockTag } from "viem";
|
|
2
|
+
import type { RawSimulationResult, SimulationTransaction } from "../../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Simulate transactions using eth_simulateV1 (viem simulateCalls).
|
|
5
|
+
*
|
|
6
|
+
* Uses stateOverride to set the user's ETH balance high enough to avoid
|
|
7
|
+
* false "insufficient balance for gas" reverts.
|
|
8
|
+
*/
|
|
9
|
+
export declare function simulateV1(params: {
|
|
10
|
+
rpcUrl: string;
|
|
11
|
+
chainId: number;
|
|
12
|
+
transactions: SimulationTransaction[];
|
|
13
|
+
blockNumber?: bigint | BlockTag;
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
}): Promise<RawSimulationResult>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { http, createPublicClient, getAddress, maxUint256, } from "viem";
|
|
2
|
+
import { ExternalServiceError, SimulationRevertedError, SimulationValidationError, } from "../../errors.js";
|
|
3
|
+
/**
|
|
4
|
+
* Simulate transactions using eth_simulateV1 (viem simulateCalls).
|
|
5
|
+
*
|
|
6
|
+
* Uses stateOverride to set the user's ETH balance high enough to avoid
|
|
7
|
+
* false "insufficient balance for gas" reverts.
|
|
8
|
+
*/
|
|
9
|
+
export async function simulateV1(params) {
|
|
10
|
+
const { rpcUrl, transactions, blockNumber, signal } = params;
|
|
11
|
+
const client = createPublicClient({
|
|
12
|
+
transport: http(rpcUrl, {
|
|
13
|
+
fetchOptions: signal ? { signal } : undefined,
|
|
14
|
+
}),
|
|
15
|
+
});
|
|
16
|
+
const firstTx = transactions[0];
|
|
17
|
+
if (!firstTx) {
|
|
18
|
+
throw new SimulationValidationError("At least one transaction is required", []);
|
|
19
|
+
}
|
|
20
|
+
const sender = getAddress(firstTx.from);
|
|
21
|
+
// eth_simulateV1 executes all calls as the same account. Reject mixed senders
|
|
22
|
+
// to avoid silently simulating under the wrong address.
|
|
23
|
+
const mixedSender = transactions.find((tx) => getAddress(tx.from) !== sender);
|
|
24
|
+
if (mixedSender) {
|
|
25
|
+
throw new SimulationValidationError("All transactions must have the same from address for eth_simulateV1", [`expected ${sender}, got ${mixedSender.from}`]);
|
|
26
|
+
}
|
|
27
|
+
const calls = transactions.map((tx) => ({
|
|
28
|
+
to: tx.to,
|
|
29
|
+
data: tx.data,
|
|
30
|
+
value: tx.value,
|
|
31
|
+
}));
|
|
32
|
+
const blockParam = typeof blockNumber === "bigint"
|
|
33
|
+
? { blockNumber }
|
|
34
|
+
: blockNumber !== undefined
|
|
35
|
+
? { blockTag: blockNumber }
|
|
36
|
+
: {};
|
|
37
|
+
try {
|
|
38
|
+
const simulationResult = await client.simulateCalls({
|
|
39
|
+
account: sender,
|
|
40
|
+
calls,
|
|
41
|
+
...blockParam,
|
|
42
|
+
// Inflate sender ETH balance to prevent false "insufficient gas" reverts.
|
|
43
|
+
// Without this, valid ERC20 flows fail when the sender has low ETH.
|
|
44
|
+
stateOverrides: [{ address: sender, balance: maxUint256 }],
|
|
45
|
+
});
|
|
46
|
+
const results = simulationResult.results;
|
|
47
|
+
if (!Array.isArray(results)) {
|
|
48
|
+
throw new ExternalServiceError("eth_simulateV1 returned unexpected response format");
|
|
49
|
+
}
|
|
50
|
+
const failedResult = results.find((r) => r.status !== "success");
|
|
51
|
+
if (failedResult) {
|
|
52
|
+
throw new SimulationRevertedError(failedResult.error?.message ?? "Simulation failed", results);
|
|
53
|
+
}
|
|
54
|
+
const rawLogs = [];
|
|
55
|
+
for (const r of results) {
|
|
56
|
+
for (const log of r.logs ?? []) {
|
|
57
|
+
rawLogs.push({
|
|
58
|
+
address: log.address,
|
|
59
|
+
topics: [...log.topics],
|
|
60
|
+
data: log.data ?? "0x",
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { logs: rawLogs };
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (error instanceof SimulationRevertedError)
|
|
68
|
+
throw error;
|
|
69
|
+
if (error instanceof ExternalServiceError)
|
|
70
|
+
throw error;
|
|
71
|
+
throw new ExternalServiceError(`eth_simulateV1 error: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -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>;
|