@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.
Files changed (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -0
  3. package/lib/cjs/errors.d.ts +50 -0
  4. package/lib/cjs/errors.js +72 -0
  5. package/lib/cjs/index.d.ts +4 -0
  6. package/lib/cjs/index.js +17 -0
  7. package/lib/cjs/package.json +1 -0
  8. package/lib/cjs/screen-addresses/index.d.ts +1 -0
  9. package/lib/cjs/screen-addresses/index.js +5 -0
  10. package/lib/cjs/screen-addresses/sanctioned-addresses.d.ts +11 -0
  11. package/lib/cjs/screen-addresses/sanctioned-addresses.js +77 -0
  12. package/lib/cjs/screen-addresses/screen-addresses.d.ts +34 -0
  13. package/lib/cjs/screen-addresses/screen-addresses.js +152 -0
  14. package/lib/cjs/simulate/authorizations/index.d.ts +2 -0
  15. package/lib/cjs/simulate/authorizations/index.js +7 -0
  16. package/lib/cjs/simulate/authorizations/resolve.d.ts +9 -0
  17. package/lib/cjs/simulate/authorizations/resolve.js +29 -0
  18. package/lib/cjs/simulate/authorizations/validate.d.ts +6 -0
  19. package/lib/cjs/simulate/authorizations/validate.js +23 -0
  20. package/lib/cjs/simulate/backends/eth-simulate-v1.d.ts +15 -0
  21. package/lib/cjs/simulate/backends/eth-simulate-v1.js +76 -0
  22. package/lib/cjs/simulate/backends/index.d.ts +2 -0
  23. package/lib/cjs/simulate/backends/index.js +7 -0
  24. package/lib/cjs/simulate/backends/tenderly-rest.d.ts +20 -0
  25. package/lib/cjs/simulate/backends/tenderly-rest.js +269 -0
  26. package/lib/cjs/simulate/index.d.ts +1 -0
  27. package/lib/cjs/simulate/index.js +5 -0
  28. package/lib/cjs/simulate/parsing/index.d.ts +1 -0
  29. package/lib/cjs/simulate/parsing/index.js +5 -0
  30. package/lib/cjs/simulate/parsing/transfers.d.ts +27 -0
  31. package/lib/cjs/simulate/parsing/transfers.js +153 -0
  32. package/lib/cjs/simulate/pipeline/build-simulation-txs.d.ts +17 -0
  33. package/lib/cjs/simulate/pipeline/build-simulation-txs.js +50 -0
  34. package/lib/cjs/simulate/pipeline/bundler-retention.d.ts +19 -0
  35. package/lib/cjs/simulate/pipeline/bundler-retention.js +90 -0
  36. package/lib/cjs/simulate/pipeline/execute-simulation.d.ts +25 -0
  37. package/lib/cjs/simulate/pipeline/execute-simulation.js +87 -0
  38. package/lib/cjs/simulate/pipeline/index.d.ts +4 -0
  39. package/lib/cjs/simulate/pipeline/index.js +11 -0
  40. package/lib/cjs/simulate/pipeline/resolve-chain.d.ts +14 -0
  41. package/lib/cjs/simulate/pipeline/resolve-chain.js +19 -0
  42. package/lib/cjs/simulate/pipeline/validate-input.d.ts +10 -0
  43. package/lib/cjs/simulate/pipeline/validate-input.js +55 -0
  44. package/lib/cjs/simulate/simulate.d.ts +26 -0
  45. package/lib/cjs/simulate/simulate.js +46 -0
  46. package/lib/cjs/types.d.ts +131 -0
  47. package/lib/cjs/types.js +2 -0
  48. package/lib/esm/errors.d.ts +50 -0
  49. package/lib/esm/errors.js +62 -0
  50. package/lib/esm/index.d.ts +4 -0
  51. package/lib/esm/index.js +5 -0
  52. package/lib/esm/package.json +1 -0
  53. package/lib/esm/screen-addresses/index.d.ts +1 -0
  54. package/lib/esm/screen-addresses/index.js +1 -0
  55. package/lib/esm/screen-addresses/sanctioned-addresses.d.ts +11 -0
  56. package/lib/esm/screen-addresses/sanctioned-addresses.js +74 -0
  57. package/lib/esm/screen-addresses/screen-addresses.d.ts +34 -0
  58. package/lib/esm/screen-addresses/screen-addresses.js +149 -0
  59. package/lib/esm/simulate/authorizations/index.d.ts +2 -0
  60. package/lib/esm/simulate/authorizations/index.js +2 -0
  61. package/lib/esm/simulate/authorizations/resolve.d.ts +9 -0
  62. package/lib/esm/simulate/authorizations/resolve.js +26 -0
  63. package/lib/esm/simulate/authorizations/validate.d.ts +6 -0
  64. package/lib/esm/simulate/authorizations/validate.js +20 -0
  65. package/lib/esm/simulate/backends/eth-simulate-v1.d.ts +15 -0
  66. package/lib/esm/simulate/backends/eth-simulate-v1.js +73 -0
  67. package/lib/esm/simulate/backends/index.d.ts +2 -0
  68. package/lib/esm/simulate/backends/index.js +2 -0
  69. package/lib/esm/simulate/backends/tenderly-rest.d.ts +20 -0
  70. package/lib/esm/simulate/backends/tenderly-rest.js +266 -0
  71. package/lib/esm/simulate/index.d.ts +1 -0
  72. package/lib/esm/simulate/index.js +1 -0
  73. package/lib/esm/simulate/parsing/index.d.ts +1 -0
  74. package/lib/esm/simulate/parsing/index.js +1 -0
  75. package/lib/esm/simulate/parsing/transfers.d.ts +27 -0
  76. package/lib/esm/simulate/parsing/transfers.js +149 -0
  77. package/lib/esm/simulate/pipeline/build-simulation-txs.d.ts +17 -0
  78. package/lib/esm/simulate/pipeline/build-simulation-txs.js +47 -0
  79. package/lib/esm/simulate/pipeline/bundler-retention.d.ts +19 -0
  80. package/lib/esm/simulate/pipeline/bundler-retention.js +87 -0
  81. package/lib/esm/simulate/pipeline/execute-simulation.d.ts +25 -0
  82. package/lib/esm/simulate/pipeline/execute-simulation.js +84 -0
  83. package/lib/esm/simulate/pipeline/index.d.ts +4 -0
  84. package/lib/esm/simulate/pipeline/index.js +4 -0
  85. package/lib/esm/simulate/pipeline/resolve-chain.d.ts +14 -0
  86. package/lib/esm/simulate/pipeline/resolve-chain.js +16 -0
  87. package/lib/esm/simulate/pipeline/validate-input.d.ts +10 -0
  88. package/lib/esm/simulate/pipeline/validate-input.js +52 -0
  89. package/lib/esm/simulate/simulate.d.ts +26 -0
  90. package/lib/esm/simulate/simulate.js +43 -0
  91. package/lib/esm/types.d.ts +131 -0
  92. package/lib/esm/types.js +1 -0
  93. package/package.json +49 -0
  94. 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,4 @@
1
+ export { buildSimulationTxs } from "./build-simulation-txs.js";
2
+ export { assertNoBundlerRetention } from "./bundler-retention.js";
3
+ export { executeSimulation } from "./execute-simulation.js";
4
+ export { validateInput } from "./validate-input.js";
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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";
@@ -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";