@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,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.assertNoBundlerRetention = assertNoBundlerRetention;
|
|
4
|
+
const blue_sdk_1 = require("@morpho-org/blue-sdk");
|
|
5
|
+
const morpho_ts_1 = require("@morpho-org/morpho-ts");
|
|
6
|
+
const viem_1 = require("viem");
|
|
7
|
+
const errors_js_1 = require("../../errors.js");
|
|
8
|
+
/**
|
|
9
|
+
* Dust tolerance for bundler retention, in raw token units.
|
|
10
|
+
*
|
|
11
|
+
* 100 units is negligible for 18-decimal tokens (~1e-16). For low-decimal
|
|
12
|
+
* tokens (e.g., 2-decimal stablecoins) this represents ~1.00 token — worth
|
|
13
|
+
* keeping in mind. The threshold is intentionally token-agnostic: its purpose
|
|
14
|
+
* is to absorb rounding noise from bundler3 routing, not to define an
|
|
15
|
+
* acceptable retention amount.
|
|
16
|
+
*/
|
|
17
|
+
const DUST_THRESHOLD = 100n;
|
|
18
|
+
function getBundlerAddresses(chainId, logger) {
|
|
19
|
+
try {
|
|
20
|
+
const addresses = (0, blue_sdk_1.getChainAddresses)(chainId);
|
|
21
|
+
if (!addresses.bundler3) {
|
|
22
|
+
// blue-sdk knows the chain but didn't catalog a bundler3 for it.
|
|
23
|
+
// Treat the same as UnsupportedChainIdError — retention check skipped.
|
|
24
|
+
logger?.warn("Chain known to blue-sdk but has no bundler3 config, retention check skipped", {
|
|
25
|
+
chainId,
|
|
26
|
+
});
|
|
27
|
+
return new Set();
|
|
28
|
+
}
|
|
29
|
+
return new Set(Object.values(addresses.bundler3)
|
|
30
|
+
.filter(morpho_ts_1.isDefined)
|
|
31
|
+
.map((addr) => (0, viem_1.getAddress)(addr)));
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (error instanceof blue_sdk_1.UnsupportedChainIdError) {
|
|
35
|
+
// Loud warn: this disables a "never bypassable" check for the chain.
|
|
36
|
+
// Consumers relying on the guarantee must handle this signal.
|
|
37
|
+
logger?.warn("Chain not supported by blue-sdk, retention check skipped", {
|
|
38
|
+
chainId,
|
|
39
|
+
});
|
|
40
|
+
return new Set();
|
|
41
|
+
}
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Assert that no value is retained by bundler3 contract addresses.
|
|
47
|
+
*
|
|
48
|
+
* Uses net flow (inbound minus outbound) per (bundler address, token) pair.
|
|
49
|
+
* Bundler3 legitimately receives tokens as an intermediary (user → bundler3 →
|
|
50
|
+
* vault), so gross inbound would fire false positives. We only flag when
|
|
51
|
+
* `|net| > DUST_THRESHOLD` — catching both **retention** (tokens stuck in
|
|
52
|
+
* bundler, positive net) and **drain** (bundler sending more than it received
|
|
53
|
+
* in this bundle, negative net — which implies a pre-existing or state-override
|
|
54
|
+
* balance is being drawn down, a red flag for pre-broadcast integrity).
|
|
55
|
+
*/
|
|
56
|
+
function assertNoBundlerRetention(params) {
|
|
57
|
+
const { chainId, transfers, logger } = params;
|
|
58
|
+
const bundlerAddresses = getBundlerAddresses(chainId, logger);
|
|
59
|
+
if (bundlerAddresses.size === 0)
|
|
60
|
+
return;
|
|
61
|
+
// Map keyed by (bundler, token) → structured entry. Avoids string parse-back.
|
|
62
|
+
const flow = new Map();
|
|
63
|
+
const recordFlow = (address, token, delta) => {
|
|
64
|
+
const key = `${address.toLowerCase()}:${token.toLowerCase()}`;
|
|
65
|
+
const existing = flow.get(key);
|
|
66
|
+
if (existing) {
|
|
67
|
+
existing.netChange += delta;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
flow.set(key, { address, token, netChange: delta });
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
for (const t of transfers) {
|
|
74
|
+
if (bundlerAddresses.has(t.to))
|
|
75
|
+
recordFlow(t.to, t.token, t.amount);
|
|
76
|
+
if (bundlerAddresses.has(t.from))
|
|
77
|
+
recordFlow(t.from, t.token, -t.amount);
|
|
78
|
+
}
|
|
79
|
+
const flagged = [...flow.values()].filter((e) => absBigInt(e.netChange) > DUST_THRESHOLD);
|
|
80
|
+
if (flagged.length > 0) {
|
|
81
|
+
throw new errors_js_1.BlacklistViolationError("Simulation detected asset transfers retained or drained by restricted bundler contracts", flagged.map((e) => ({
|
|
82
|
+
address: e.address,
|
|
83
|
+
token: e.token,
|
|
84
|
+
netRetained: e.netChange.toString(),
|
|
85
|
+
})));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function absBigInt(x) {
|
|
89
|
+
return x < 0n ? -x : x;
|
|
90
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { BlockTag } from "viem";
|
|
2
|
+
import type { RawSimulationResult, SimulationConfig, SimulationTransaction } from "../../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Stage 4 of the simulate() pipeline.
|
|
5
|
+
*
|
|
6
|
+
* Dispatches the transaction bundle to the available backend with a shared timeout
|
|
7
|
+
* budget:
|
|
8
|
+
* - If Tenderly is configured for the chain, it gets `TENDERLY_BUDGET_RATIO` (60%) of
|
|
9
|
+
* the timeout. On `ExternalServiceError`, falls back to `eth_simulateV1` with
|
|
10
|
+
* `max(remaining, FALLBACK_MIN_BUDGET_MS)` so the fallback still has a viable
|
|
11
|
+
* window when Tenderly eats its full slice. `SimulationRevertedError` (contract
|
|
12
|
+
* revert) propagates immediately without retry — a revert is a property of the
|
|
13
|
+
* bundle, not the backend.
|
|
14
|
+
* - If only `eth_simulateV1` is configured, it gets the full timeout.
|
|
15
|
+
*
|
|
16
|
+
* `shareable` is Tenderly-only: when true, the backend persists the simulation and
|
|
17
|
+
* returns a shareable URL. `eth_simulateV1` ignores it (no persistence concept).
|
|
18
|
+
*/
|
|
19
|
+
export declare function executeSimulation(params: {
|
|
20
|
+
config: SimulationConfig;
|
|
21
|
+
chainId: number;
|
|
22
|
+
transactions: SimulationTransaction[];
|
|
23
|
+
blockNumber?: bigint | BlockTag;
|
|
24
|
+
shareable: boolean;
|
|
25
|
+
}): Promise<RawSimulationResult>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.executeSimulation = executeSimulation;
|
|
4
|
+
const errors_js_1 = require("../../errors.js");
|
|
5
|
+
const index_js_1 = require("../backends/index.js");
|
|
6
|
+
const resolve_chain_js_1 = require("./resolve-chain.js");
|
|
7
|
+
/** Total budget for a single `simulate()` call across both backends. */
|
|
8
|
+
const DEFAULT_TIMEOUT_MS = 5000;
|
|
9
|
+
/** Fraction of `timeoutMs` given to Tenderly before falling back to `eth_simulateV1`. */
|
|
10
|
+
const TENDERLY_BUDGET_RATIO = 0.6;
|
|
11
|
+
/**
|
|
12
|
+
* Minimum budget (ms) handed to the `eth_simulateV1` fallback when Tenderly fails.
|
|
13
|
+
*
|
|
14
|
+
* Without a floor, if Tenderly exhausts its slice and dies at the deadline, the
|
|
15
|
+
* fallback would get ~0 ms and abort immediately — turning a degraded-Tenderly
|
|
16
|
+
* scenario into a total outage even when the fallback backend is healthy. A
|
|
17
|
+
* modest floor keeps the fallback viable at the cost of the overall
|
|
18
|
+
* `timeoutMs` being treated as a soft ceiling.
|
|
19
|
+
*/
|
|
20
|
+
const FALLBACK_MIN_BUDGET_MS = 1500;
|
|
21
|
+
/**
|
|
22
|
+
* Stage 4 of the simulate() pipeline.
|
|
23
|
+
*
|
|
24
|
+
* Dispatches the transaction bundle to the available backend with a shared timeout
|
|
25
|
+
* budget:
|
|
26
|
+
* - If Tenderly is configured for the chain, it gets `TENDERLY_BUDGET_RATIO` (60%) of
|
|
27
|
+
* the timeout. On `ExternalServiceError`, falls back to `eth_simulateV1` with
|
|
28
|
+
* `max(remaining, FALLBACK_MIN_BUDGET_MS)` so the fallback still has a viable
|
|
29
|
+
* window when Tenderly eats its full slice. `SimulationRevertedError` (contract
|
|
30
|
+
* revert) propagates immediately without retry — a revert is a property of the
|
|
31
|
+
* bundle, not the backend.
|
|
32
|
+
* - If only `eth_simulateV1` is configured, it gets the full timeout.
|
|
33
|
+
*
|
|
34
|
+
* `shareable` is Tenderly-only: when true, the backend persists the simulation and
|
|
35
|
+
* returns a shareable URL. `eth_simulateV1` ignores it (no persistence concept).
|
|
36
|
+
*/
|
|
37
|
+
async function executeSimulation(params) {
|
|
38
|
+
const { config, chainId, transactions, blockNumber, shareable } = params;
|
|
39
|
+
const chain = (0, resolve_chain_js_1.resolveChain)(config, chainId);
|
|
40
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
41
|
+
const deadline = Date.now() + timeoutMs;
|
|
42
|
+
if (chain.tenderlySupported && config.tenderlyRest) {
|
|
43
|
+
const tenderlyTimeout = Math.floor(timeoutMs * TENDERLY_BUDGET_RATIO);
|
|
44
|
+
try {
|
|
45
|
+
return await (0, index_js_1.simulateTenderlyRest)({
|
|
46
|
+
config: config.tenderlyRest,
|
|
47
|
+
chainId,
|
|
48
|
+
transactions,
|
|
49
|
+
blockNumber,
|
|
50
|
+
shareable,
|
|
51
|
+
signal: AbortSignal.timeout(tenderlyTimeout),
|
|
52
|
+
logger: config.logger,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (!(error instanceof errors_js_1.ExternalServiceError))
|
|
57
|
+
throw error;
|
|
58
|
+
config.logger?.warn("Tenderly simulation failed, attempting fallback", {
|
|
59
|
+
chainId,
|
|
60
|
+
error: error.message,
|
|
61
|
+
cause: error.cause,
|
|
62
|
+
});
|
|
63
|
+
if (!chain.simulateV1Url)
|
|
64
|
+
throw error;
|
|
65
|
+
// Give the fallback a reasonable minimum even if Tenderly consumed the
|
|
66
|
+
// whole budget — otherwise slow-Tenderly == total outage.
|
|
67
|
+
const fallbackBudget = Math.max(deadline - Date.now(), FALLBACK_MIN_BUDGET_MS);
|
|
68
|
+
return await (0, index_js_1.simulateV1)({
|
|
69
|
+
rpcUrl: chain.simulateV1Url,
|
|
70
|
+
chainId,
|
|
71
|
+
transactions,
|
|
72
|
+
blockNumber,
|
|
73
|
+
signal: AbortSignal.timeout(fallbackBudget),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (!chain.simulateV1Url) {
|
|
78
|
+
throw new errors_js_1.UnsupportedChainError(chainId);
|
|
79
|
+
}
|
|
80
|
+
return await (0, index_js_1.simulateV1)({
|
|
81
|
+
rpcUrl: chain.simulateV1Url,
|
|
82
|
+
chainId,
|
|
83
|
+
transactions,
|
|
84
|
+
blockNumber,
|
|
85
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateInput = exports.executeSimulation = exports.assertNoBundlerRetention = exports.buildSimulationTxs = void 0;
|
|
4
|
+
var build_simulation_txs_js_1 = require("./build-simulation-txs.js");
|
|
5
|
+
Object.defineProperty(exports, "buildSimulationTxs", { enumerable: true, get: function () { return build_simulation_txs_js_1.buildSimulationTxs; } });
|
|
6
|
+
var bundler_retention_js_1 = require("./bundler-retention.js");
|
|
7
|
+
Object.defineProperty(exports, "assertNoBundlerRetention", { enumerable: true, get: function () { return bundler_retention_js_1.assertNoBundlerRetention; } });
|
|
8
|
+
var execute_simulation_js_1 = require("./execute-simulation.js");
|
|
9
|
+
Object.defineProperty(exports, "executeSimulation", { enumerable: true, get: function () { return execute_simulation_js_1.executeSimulation; } });
|
|
10
|
+
var validate_input_js_1 = require("./validate-input.js");
|
|
11
|
+
Object.defineProperty(exports, "validateInput", { enumerable: true, get: function () { return validate_input_js_1.validateInput; } });
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SimulationConfig } from "../../types.js";
|
|
2
|
+
interface ChainCapability {
|
|
3
|
+
tenderlySupported: boolean;
|
|
4
|
+
simulateV1Url?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Stage 2 of the simulate() pipeline.
|
|
8
|
+
*
|
|
9
|
+
* Determines which backend(s) are available for a chainId. Throws
|
|
10
|
+
* `UnsupportedChainError` when neither Tenderly nor `eth_simulateV1` is reachable
|
|
11
|
+
* for the requested chain.
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveChain(config: SimulationConfig, chainId: number): ChainCapability;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveChain = resolveChain;
|
|
4
|
+
const errors_js_1 = require("../../errors.js");
|
|
5
|
+
/**
|
|
6
|
+
* Stage 2 of the simulate() pipeline.
|
|
7
|
+
*
|
|
8
|
+
* Determines which backend(s) are available for a chainId. Throws
|
|
9
|
+
* `UnsupportedChainError` when neither Tenderly nor `eth_simulateV1` is reachable
|
|
10
|
+
* for the requested chain.
|
|
11
|
+
*/
|
|
12
|
+
function resolveChain(config, chainId) {
|
|
13
|
+
const tenderlySupported = config.tenderlyRest?.supportedChainIds.has(chainId) ?? false;
|
|
14
|
+
const simulateV1Url = config.chains.get(chainId)?.simulateV1Url;
|
|
15
|
+
if (!tenderlySupported && !simulateV1Url) {
|
|
16
|
+
throw new errors_js_1.UnsupportedChainError(chainId);
|
|
17
|
+
}
|
|
18
|
+
return { tenderlySupported, simulateV1Url };
|
|
19
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SimulateParams } from "../../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Stage 1 of the simulate() pipeline.
|
|
4
|
+
*
|
|
5
|
+
* Throws `SimulationValidationError` with a `fieldErrors[]` list on any invalid input:
|
|
6
|
+
* empty transactions, malformed / zero-addr fields, missing `data`, negative `value`,
|
|
7
|
+
* bad `chainId`, or mixed senders (all txs in a bundle must share the same `from`).
|
|
8
|
+
* Also runs `validateAuthorizations` on the optional authorizations array.
|
|
9
|
+
*/
|
|
10
|
+
export declare function validateInput(params: SimulateParams): void;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateInput = validateInput;
|
|
4
|
+
const viem_1 = require("viem");
|
|
5
|
+
const errors_js_1 = require("../../errors.js");
|
|
6
|
+
const index_js_1 = require("../authorizations/index.js");
|
|
7
|
+
/**
|
|
8
|
+
* Stage 1 of the simulate() pipeline.
|
|
9
|
+
*
|
|
10
|
+
* Throws `SimulationValidationError` with a `fieldErrors[]` list on any invalid input:
|
|
11
|
+
* empty transactions, malformed / zero-addr fields, missing `data`, negative `value`,
|
|
12
|
+
* bad `chainId`, or mixed senders (all txs in a bundle must share the same `from`).
|
|
13
|
+
* Also runs `validateAuthorizations` on the optional authorizations array.
|
|
14
|
+
*/
|
|
15
|
+
function validateInput(params) {
|
|
16
|
+
const errors = [];
|
|
17
|
+
if (!Number.isInteger(params.chainId) || params.chainId <= 0) {
|
|
18
|
+
errors.push(`chainId: must be a positive integer (got ${params.chainId})`);
|
|
19
|
+
}
|
|
20
|
+
const transactions = params.transactions ?? [];
|
|
21
|
+
if (transactions.length === 0) {
|
|
22
|
+
errors.push("transactions: must contain at least 1 transaction");
|
|
23
|
+
}
|
|
24
|
+
for (let i = 0; i < transactions.length; i++) {
|
|
25
|
+
const tx = transactions[i];
|
|
26
|
+
if (!tx.from || !(0, viem_1.isAddress)(tx.from) || tx.from === viem_1.zeroAddress) {
|
|
27
|
+
errors.push(`transactions[${i}].from: must be a valid non-zero address`);
|
|
28
|
+
}
|
|
29
|
+
if (!tx.to || !(0, viem_1.isAddress)(tx.to) || tx.to === viem_1.zeroAddress) {
|
|
30
|
+
errors.push(`transactions[${i}].to: must be a valid non-zero address`);
|
|
31
|
+
}
|
|
32
|
+
if (!tx.data) {
|
|
33
|
+
errors.push(`transactions[${i}].data: must be provided`);
|
|
34
|
+
}
|
|
35
|
+
if (tx.value !== undefined && tx.value < 0n) {
|
|
36
|
+
errors.push(`transactions[${i}].value: must be non-negative`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Same-sender check uses RAW `.from` strings (lowercased) so the invariant
|
|
40
|
+
// fires even when some tx `from`s are malformed — individual txs already
|
|
41
|
+
// flagged above, but we still want one loud signal that the bundle's
|
|
42
|
+
// senders don't match.
|
|
43
|
+
if (transactions.length > 1) {
|
|
44
|
+
const distinct = new Set(transactions.map((tx) => (tx.from ?? "").toLowerCase()));
|
|
45
|
+
if (distinct.size > 1) {
|
|
46
|
+
errors.push("transactions: all transactions must share the same from address");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (params.authorizations) {
|
|
50
|
+
errors.push(...(0, index_js_1.validateAuthorizations)(params.authorizations));
|
|
51
|
+
}
|
|
52
|
+
if (errors.length > 0) {
|
|
53
|
+
throw new errors_js_1.SimulationValidationError("Invalid simulation input", errors);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { SimulateParams, SimulationConfig, SimulationResult } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Simulate a bundle of EVM transactions.
|
|
4
|
+
*
|
|
5
|
+
* Validates input → resolves authorizations into prepended approve txs → runs the bundle
|
|
6
|
+
* through Tenderly REST (primary) or `eth_simulateV1` (fallback) with a shared timeout
|
|
7
|
+
* budget → parses ERC20/WETH transfers from logs → asserts no funds are retained by
|
|
8
|
+
* `bundler3` → returns the full result set. The caller reads whichever fields they need:
|
|
9
|
+
*
|
|
10
|
+
* - `transfers` + `tenderlyUrl` → user-facing preview.
|
|
11
|
+
* - `simulationTxs` + `transfers` → server-side verification before broadcast, then pass
|
|
12
|
+
* both to `screenAddresses` for compliance.
|
|
13
|
+
*
|
|
14
|
+
* Throws `SimulationValidationError` for bad input, `UnsupportedChainError` when the chain
|
|
15
|
+
* is not configured, `SimulationRevertedError` if the bundle reverts,
|
|
16
|
+
* `BlacklistViolationError` if bundler retention is detected, or `ExternalServiceError` if
|
|
17
|
+
* both backends are unavailable within the timeout budget.
|
|
18
|
+
*/
|
|
19
|
+
export declare function simulate(config: SimulationConfig, params: SimulateParams, options?: {
|
|
20
|
+
/**
|
|
21
|
+
* When true, persist the simulation in Tenderly and return a shareable
|
|
22
|
+
* `tenderlyUrl` in the result. Only honored when the Tenderly backend runs
|
|
23
|
+
* (not the `eth_simulateV1` fallback). Defaults to `false`.
|
|
24
|
+
*/
|
|
25
|
+
shareable?: boolean;
|
|
26
|
+
}): Promise<SimulationResult>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.simulate = simulate;
|
|
4
|
+
const index_js_1 = require("./parsing/index.js");
|
|
5
|
+
const index_js_2 = require("./pipeline/index.js");
|
|
6
|
+
/**
|
|
7
|
+
* Simulate a bundle of EVM transactions.
|
|
8
|
+
*
|
|
9
|
+
* Validates input → resolves authorizations into prepended approve txs → runs the bundle
|
|
10
|
+
* through Tenderly REST (primary) or `eth_simulateV1` (fallback) with a shared timeout
|
|
11
|
+
* budget → parses ERC20/WETH transfers from logs → asserts no funds are retained by
|
|
12
|
+
* `bundler3` → returns the full result set. The caller reads whichever fields they need:
|
|
13
|
+
*
|
|
14
|
+
* - `transfers` + `tenderlyUrl` → user-facing preview.
|
|
15
|
+
* - `simulationTxs` + `transfers` → server-side verification before broadcast, then pass
|
|
16
|
+
* both to `screenAddresses` for compliance.
|
|
17
|
+
*
|
|
18
|
+
* Throws `SimulationValidationError` for bad input, `UnsupportedChainError` when the chain
|
|
19
|
+
* is not configured, `SimulationRevertedError` if the bundle reverts,
|
|
20
|
+
* `BlacklistViolationError` if bundler retention is detected, or `ExternalServiceError` if
|
|
21
|
+
* both backends are unavailable within the timeout budget.
|
|
22
|
+
*/
|
|
23
|
+
async function simulate(config, params, options = {}) {
|
|
24
|
+
const { shareable = false } = options;
|
|
25
|
+
(0, index_js_2.validateInput)(params);
|
|
26
|
+
const simulationTxs = (0, index_js_2.buildSimulationTxs)(params);
|
|
27
|
+
const result = await (0, index_js_2.executeSimulation)({
|
|
28
|
+
config,
|
|
29
|
+
chainId: params.chainId,
|
|
30
|
+
transactions: simulationTxs,
|
|
31
|
+
blockNumber: params.blockNumber,
|
|
32
|
+
shareable,
|
|
33
|
+
});
|
|
34
|
+
const transfers = (0, index_js_1.parseTransfers)(result.logs, config.logger);
|
|
35
|
+
(0, index_js_2.assertNoBundlerRetention)({
|
|
36
|
+
chainId: params.chainId,
|
|
37
|
+
transfers,
|
|
38
|
+
logger: config.logger,
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
simulationTxs,
|
|
42
|
+
transfers,
|
|
43
|
+
tenderlyUrl: result.tenderlyUrl,
|
|
44
|
+
assetChanges: result.rawAssetChanges,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { Address, BlockTag, Hex } from "viem";
|
|
2
|
+
/**
|
|
3
|
+
* Credentials and routing for the Tenderly REST simulation backend.
|
|
4
|
+
* Omit from `SimulationConfig.tenderlyRest` to disable the Tenderly path entirely.
|
|
5
|
+
*/
|
|
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>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Per-chain capabilities. Stored as a `Map` value in
|
|
17
|
+
* `SimulationConfig.chains`, keyed by chain ID.
|
|
18
|
+
*/
|
|
19
|
+
export interface ChainSimulationConfig {
|
|
20
|
+
/** JSON-RPC URL supporting `eth_simulateV1`. undefined = not available for this chain. */
|
|
21
|
+
simulateV1Url?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Top-level configuration for `simulate`.
|
|
25
|
+
*
|
|
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`.
|
|
30
|
+
*/
|
|
31
|
+
export interface SimulationConfig {
|
|
32
|
+
/** Tenderly REST API config. undefined = Tenderly not available. */
|
|
33
|
+
tenderlyRest?: TenderlyRestConfig;
|
|
34
|
+
/** Per-chain simulation capabilities. */
|
|
35
|
+
chains: Map<number, ChainSimulationConfig>;
|
|
36
|
+
logger?: SimulationLogger;
|
|
37
|
+
/**
|
|
38
|
+
* Overall timeout budget in ms (default 5000). Tenderly gets ~60% of budget.
|
|
39
|
+
* On timeout/failure, fallback gets remaining time (deadline - now).
|
|
40
|
+
*/
|
|
41
|
+
timeoutMs?: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* A single EVM call to simulate. `from` must be identical across all transactions
|
|
45
|
+
* in a bundle — the orchestrator rejects mixed senders with `SimulationValidationError`.
|
|
46
|
+
*/
|
|
47
|
+
export interface SimulationTransaction {
|
|
48
|
+
from: Address;
|
|
49
|
+
to: Address;
|
|
50
|
+
data: Hex;
|
|
51
|
+
value?: bigint;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* How the caller expresses a token authorization that must be in place before the
|
|
55
|
+
* main transactions run. The package decides HOW to simulate each one:
|
|
56
|
+
* - "approval" → prepend tx as-is
|
|
57
|
+
* - "signature" → today: encode approve(spender, amount); future: ecrecover override?
|
|
58
|
+
*/
|
|
59
|
+
export type SimulationAuthorization = {
|
|
60
|
+
type: "approval";
|
|
61
|
+
transaction: SimulationTransaction;
|
|
62
|
+
} | {
|
|
63
|
+
type: "signature";
|
|
64
|
+
token: Address;
|
|
65
|
+
spender: Address;
|
|
66
|
+
/** Defaults to maxUint256. Caller can set explicit amount for tokens that don't support max approval. */
|
|
67
|
+
amount?: bigint;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* A parsed ERC20 / WETH9 transfer extracted from simulation logs. Returned in
|
|
71
|
+
* `SimulationResult.transfers`.
|
|
72
|
+
*/
|
|
73
|
+
export interface Transfer {
|
|
74
|
+
token: Address;
|
|
75
|
+
from: Address;
|
|
76
|
+
to: Address;
|
|
77
|
+
amount: bigint;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Happy-path return of `simulate`. All failures throw typed errors. The same shape is
|
|
81
|
+
* returned regardless of `options.shareable` — fields unused by your use case can be
|
|
82
|
+
* ignored:
|
|
83
|
+
*
|
|
84
|
+
* - Preview (`shareable: true`): read `transfers` + `tenderlyUrl`.
|
|
85
|
+
* - Verify (`shareable: false`, default): read `simulationTxs` + `transfers`, pass both
|
|
86
|
+
* to `screenAddresses`.
|
|
87
|
+
*
|
|
88
|
+
* `tenderlyUrl` is set only when `shareable: true` AND the Tenderly backend ran
|
|
89
|
+
* successfully (not the `eth_simulateV1` fallback). `assetChanges` is Tenderly-only
|
|
90
|
+
* raw data.
|
|
91
|
+
*/
|
|
92
|
+
export interface SimulationResult {
|
|
93
|
+
/** The full resolved transaction list (including prepended authorization txs). */
|
|
94
|
+
simulationTxs: SimulationTransaction[];
|
|
95
|
+
/** Parsed ERC-20 / WETH9 transfers from the simulation. */
|
|
96
|
+
transfers: Transfer[];
|
|
97
|
+
/** Shareable Tenderly URL. Present only when `shareable: true` and Tenderly ran (not fallback). */
|
|
98
|
+
tenderlyUrl?: string;
|
|
99
|
+
/** Raw Tenderly `asset_changes` payload. Opaque `unknown` — do not destructure without validation. */
|
|
100
|
+
assetChanges?: unknown;
|
|
101
|
+
}
|
|
102
|
+
/** Minimal structured logger the package calls for warnings and info. */
|
|
103
|
+
export interface SimulationLogger {
|
|
104
|
+
info(message: string, data?: Record<string, unknown>): void;
|
|
105
|
+
warn(message: string, data?: Record<string, unknown>): void;
|
|
106
|
+
error(message: string, data?: Record<string, unknown>): void;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Input to `simulate`. Pin a `blockNumber` for deterministic / historical
|
|
110
|
+
* simulation; omit to simulate against `latest`.
|
|
111
|
+
*/
|
|
112
|
+
export interface SimulateParams {
|
|
113
|
+
chainId: number;
|
|
114
|
+
transactions: SimulationTransaction[];
|
|
115
|
+
authorizations?: SimulationAuthorization[];
|
|
116
|
+
blockNumber?: bigint | BlockTag;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Internal raw result from a simulation adapter before normalization.
|
|
120
|
+
* Both Tenderly and simulateV1 produce this.
|
|
121
|
+
*/
|
|
122
|
+
export interface RawSimulationResult {
|
|
123
|
+
logs: RawLog[];
|
|
124
|
+
tenderlyUrl?: string;
|
|
125
|
+
rawAssetChanges?: unknown;
|
|
126
|
+
}
|
|
127
|
+
export interface RawLog {
|
|
128
|
+
address: Address;
|
|
129
|
+
topics: Hex[];
|
|
130
|
+
data: Hex;
|
|
131
|
+
}
|
package/lib/cjs/types.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base error for all simulation package errors.
|
|
3
|
+
* Transport-agnostic — no HTTP status codes.
|
|
4
|
+
* Consumers use try/catch + instanceof to handle specific error types.
|
|
5
|
+
*/
|
|
6
|
+
export declare abstract class SimulationPackageError extends Error {
|
|
7
|
+
abstract readonly code: string;
|
|
8
|
+
constructor(message: string, options?: ErrorOptions);
|
|
9
|
+
}
|
|
10
|
+
/** Transaction would revert on-chain. Not bypassable. */
|
|
11
|
+
export declare class SimulationRevertedError extends SimulationPackageError {
|
|
12
|
+
readonly reason: string | undefined;
|
|
13
|
+
readonly details?: unknown | undefined;
|
|
14
|
+
readonly code = "SIMULATION_REVERTED";
|
|
15
|
+
constructor(reason: string | undefined, details?: unknown | undefined);
|
|
16
|
+
}
|
|
17
|
+
interface RetainedAsset {
|
|
18
|
+
address: string | undefined;
|
|
19
|
+
token: string | undefined;
|
|
20
|
+
netRetained: string;
|
|
21
|
+
}
|
|
22
|
+
/** Funds would flow to bundler3 contract addresses. Never bypassable. */
|
|
23
|
+
export declare class BlacklistViolationError extends SimulationPackageError {
|
|
24
|
+
readonly assetChanges?: RetainedAsset[] | undefined;
|
|
25
|
+
readonly code = "BLACKLIST_ERROR";
|
|
26
|
+
constructor(message: string, assetChanges?: RetainedAsset[] | undefined);
|
|
27
|
+
}
|
|
28
|
+
/** Sanctioned or compromised address detected. Never bypassable. */
|
|
29
|
+
export declare class AddressScreeningError extends SimulationPackageError {
|
|
30
|
+
readonly addresses: string[];
|
|
31
|
+
readonly code = "SCREENING_ERROR";
|
|
32
|
+
constructor(message: string, addresses: string[]);
|
|
33
|
+
}
|
|
34
|
+
/** Tenderly or RPC service is down. Bypassable — user can proceed. */
|
|
35
|
+
export declare class ExternalServiceError extends SimulationPackageError {
|
|
36
|
+
readonly code = "EXTERNAL_SERVICE_ERROR";
|
|
37
|
+
}
|
|
38
|
+
/** Bad input to the simulation functions. Not bypassable. */
|
|
39
|
+
export declare class SimulationValidationError extends SimulationPackageError {
|
|
40
|
+
readonly fieldErrors?: string[] | undefined;
|
|
41
|
+
readonly code = "VALIDATION_ERROR";
|
|
42
|
+
constructor(message: string, fieldErrors?: string[] | undefined);
|
|
43
|
+
}
|
|
44
|
+
/** Chain ID not configured for any simulation method. Not bypassable. */
|
|
45
|
+
export declare class UnsupportedChainError extends SimulationPackageError {
|
|
46
|
+
readonly chainId: number;
|
|
47
|
+
readonly code = "UNSUPPORTED_CHAIN";
|
|
48
|
+
constructor(chainId: number);
|
|
49
|
+
}
|
|
50
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base error for all simulation package errors.
|
|
3
|
+
* Transport-agnostic — no HTTP status codes.
|
|
4
|
+
* Consumers use try/catch + instanceof to handle specific error types.
|
|
5
|
+
*/
|
|
6
|
+
export class SimulationPackageError extends Error {
|
|
7
|
+
constructor(message, options) {
|
|
8
|
+
super(message, options);
|
|
9
|
+
this.name = this.constructor.name;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** Transaction would revert on-chain. Not bypassable. */
|
|
13
|
+
export class SimulationRevertedError extends SimulationPackageError {
|
|
14
|
+
reason;
|
|
15
|
+
details;
|
|
16
|
+
code = "SIMULATION_REVERTED";
|
|
17
|
+
constructor(reason, details) {
|
|
18
|
+
super(reason ?? "Transaction simulation reverted");
|
|
19
|
+
this.reason = reason;
|
|
20
|
+
this.details = details;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Funds would flow to bundler3 contract addresses. Never bypassable. */
|
|
24
|
+
export class BlacklistViolationError extends SimulationPackageError {
|
|
25
|
+
assetChanges;
|
|
26
|
+
code = "BLACKLIST_ERROR";
|
|
27
|
+
constructor(message, assetChanges) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.assetChanges = assetChanges;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Sanctioned or compromised address detected. Never bypassable. */
|
|
33
|
+
export class AddressScreeningError extends SimulationPackageError {
|
|
34
|
+
addresses;
|
|
35
|
+
code = "SCREENING_ERROR";
|
|
36
|
+
constructor(message, addresses) {
|
|
37
|
+
super(message);
|
|
38
|
+
this.addresses = addresses;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Tenderly or RPC service is down. Bypassable — user can proceed. */
|
|
42
|
+
export class ExternalServiceError extends SimulationPackageError {
|
|
43
|
+
code = "EXTERNAL_SERVICE_ERROR";
|
|
44
|
+
}
|
|
45
|
+
/** Bad input to the simulation functions. Not bypassable. */
|
|
46
|
+
export class SimulationValidationError extends SimulationPackageError {
|
|
47
|
+
fieldErrors;
|
|
48
|
+
code = "VALIDATION_ERROR";
|
|
49
|
+
constructor(message, fieldErrors) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.fieldErrors = fieldErrors;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Chain ID not configured for any simulation method. Not bypassable. */
|
|
55
|
+
export class UnsupportedChainError extends SimulationPackageError {
|
|
56
|
+
chainId;
|
|
57
|
+
code = "UNSUPPORTED_CHAIN";
|
|
58
|
+
constructor(chainId) {
|
|
59
|
+
super(`Chain ${chainId} is not configured for simulation`);
|
|
60
|
+
this.chainId = chainId;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { screenAddresses } from "./screen-addresses/index.js";
|
|
2
|
+
export { simulate } from "./simulate/index.js";
|
|
3
|
+
export type { ChainSimulationConfig, SimulateParams, SimulationAuthorization, SimulationConfig, SimulationLogger, SimulationResult, SimulationTransaction, TenderlyRestConfig, Transfer, } from "./types.js";
|
|
4
|
+
export { SimulationPackageError, SimulationRevertedError, BlacklistViolationError, AddressScreeningError, ExternalServiceError, SimulationValidationError, UnsupportedChainError, } from "./errors.js";
|
package/lib/esm/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Public fns (via feature-folder barrels)
|
|
2
|
+
export { screenAddresses } from "./screen-addresses/index.js";
|
|
3
|
+
export { simulate } from "./simulate/index.js";
|
|
4
|
+
// Errors (for instanceof checks by consumers)
|
|
5
|
+
export { SimulationPackageError, SimulationRevertedError, BlacklistViolationError, AddressScreeningError, ExternalServiceError, SimulationValidationError, UnsupportedChainError, } from "./errors.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { screenAddresses } from "./screen-addresses.js";
|