@morpho-org/evm-simulation 2.0.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +22 -30
  2. package/lib/cjs/errors.d.ts +0 -6
  3. package/lib/cjs/errors.js +1 -11
  4. package/lib/cjs/index.d.ts +2 -3
  5. package/lib/cjs/index.js +3 -6
  6. package/lib/cjs/simulate/backends/index.d.ts +1 -1
  7. package/lib/cjs/simulate/backends/index.js +3 -3
  8. package/lib/cjs/simulate/backends/tenderly-rpc.d.ts +23 -0
  9. package/lib/cjs/simulate/backends/tenderly-rpc.js +176 -0
  10. package/lib/cjs/simulate/parsing/transfers.js +3 -1
  11. package/lib/cjs/simulate/pipeline/execute-simulation.d.ts +6 -10
  12. package/lib/cjs/simulate/pipeline/execute-simulation.js +11 -18
  13. package/lib/cjs/simulate/pipeline/resolve-chain.d.ts +6 -5
  14. package/lib/cjs/simulate/pipeline/resolve-chain.js +10 -7
  15. package/lib/cjs/simulate/simulate.d.ts +16 -21
  16. package/lib/cjs/simulate/simulate.js +16 -18
  17. package/lib/cjs/types.d.ts +22 -29
  18. package/lib/esm/errors.d.ts +0 -6
  19. package/lib/esm/errors.js +0 -9
  20. package/lib/esm/index.d.ts +2 -3
  21. package/lib/esm/index.js +1 -2
  22. package/lib/esm/simulate/backends/index.d.ts +1 -1
  23. package/lib/esm/simulate/backends/index.js +1 -1
  24. package/lib/esm/simulate/backends/tenderly-rpc.d.ts +23 -0
  25. package/lib/esm/simulate/backends/tenderly-rpc.js +173 -0
  26. package/lib/esm/simulate/parsing/transfers.js +3 -1
  27. package/lib/esm/simulate/pipeline/execute-simulation.d.ts +6 -10
  28. package/lib/esm/simulate/pipeline/execute-simulation.js +12 -19
  29. package/lib/esm/simulate/pipeline/resolve-chain.d.ts +6 -5
  30. package/lib/esm/simulate/pipeline/resolve-chain.js +10 -7
  31. package/lib/esm/simulate/simulate.d.ts +16 -21
  32. package/lib/esm/simulate/simulate.js +16 -18
  33. package/lib/esm/types.d.ts +22 -29
  34. package/package.json +10 -6
  35. package/src/index.ts +1 -3
  36. package/lib/cjs/screen-addresses/index.d.ts +0 -1
  37. package/lib/cjs/screen-addresses/index.js +0 -5
  38. package/lib/cjs/screen-addresses/sanctioned-addresses.d.ts +0 -11
  39. package/lib/cjs/screen-addresses/sanctioned-addresses.js +0 -77
  40. package/lib/cjs/screen-addresses/screen-addresses.d.ts +0 -50
  41. package/lib/cjs/screen-addresses/screen-addresses.js +0 -171
  42. package/lib/cjs/simulate/backends/tenderly-rest.d.ts +0 -20
  43. package/lib/cjs/simulate/backends/tenderly-rest.js +0 -279
  44. package/lib/esm/screen-addresses/index.d.ts +0 -1
  45. package/lib/esm/screen-addresses/index.js +0 -1
  46. package/lib/esm/screen-addresses/sanctioned-addresses.d.ts +0 -11
  47. package/lib/esm/screen-addresses/sanctioned-addresses.js +0 -74
  48. package/lib/esm/screen-addresses/screen-addresses.d.ts +0 -50
  49. package/lib/esm/screen-addresses/screen-addresses.js +0 -168
  50. package/lib/esm/simulate/backends/tenderly-rest.d.ts +0 -20
  51. package/lib/esm/simulate/backends/tenderly-rest.js +0 -276
@@ -8,20 +8,19 @@ const index_js_2 = require("./pipeline/index.js");
8
8
  * Simulate a bundle of EVM transactions.
9
9
  *
10
10
  * Validates input → resolves authorizations into prepended approve txs → runs the bundle
11
- * through Tenderly REST (primary) or `eth_simulateV1` (fallback) with a shared timeout
11
+ * through Tenderly RPC (primary) or `eth_simulateV1` (fallback) with a shared timeout
12
12
  * budget → parses ERC20/WETH transfers from per-tx logs → asserts no funds are retained
13
13
  * by `bundler3` → returns the full result set. The caller reads whichever fields they need:
14
14
  *
15
- * - `transfers` + `tenderlyUrl` → user-facing preview.
16
- * - `simulationTxs` + `transfers` → server-side verification before broadcast, then pass
17
- * both to `screenAddresses` for compliance.
15
+ * - `transfers` → user-facing preview / server-side verification.
16
+ * - `simulationTxs` + `transfers` → server-side verification before broadcast.
18
17
  * - `calls[i]` → per-tx raw backend output (`logs`, `status`, `returnData`, `gasUsed`,
19
18
  * and Tenderly-only `assetChanges`). Aligned 1:1 with `simulationTxs[i]`.
20
19
  * - `transfers[k].txIdx` → index into `simulationTxs` of the tx that emitted the
21
20
  * underlying log; consumers map back via `simulationTxs[transfer.txIdx]`.
22
21
  *
23
- * @param config - Backend configuration: Tenderly credentials, per-chain RPC URLs, optional
24
- * logger, and the overall timeout budget.
22
+ * @param config - Backend configuration: per-chain Tenderly RPC and/or `eth_simulateV1`
23
+ * URL, optional logger, and the overall timeout budget.
25
24
  * @param params - Per-call simulation input.
26
25
  * @param params.chainId - Chain id the bundle targets.
27
26
  * @param params.transactions - The bundle's transactions, in execution order. All must share the
@@ -29,10 +28,6 @@ const index_js_2 = require("./pipeline/index.js");
29
28
  * @param params.authorizations - Optional token authorizations resolved into prepended approve
30
29
  * transactions before the main bundle runs.
31
30
  * @param params.blockNumber - Optional pinned block number or `BlockTag`. Defaults to `latest`.
32
- * @param options - Per-call simulation options.
33
- * @param options.shareable - When `true`, persists the simulation in Tenderly and returns a
34
- * shareable `tenderlyUrl`. Honored only on the Tenderly backend, not on the `eth_simulateV1`
35
- * fallback. Defaults to `false`.
36
31
  * @throws {SimulationValidationError} for invalid input (mixed senders, bad addresses,
37
32
  * empty transactions, malformed authorizations).
38
33
  * @throws {UnsupportedChainError} when the chain is not configured for any backend.
@@ -44,14 +39,21 @@ const index_js_2 = require("./pipeline/index.js");
44
39
  * not match the resolved `simulationTxs` — refusing to map transfers with mismatched
45
40
  * per-tx output.
46
41
  * @returns A {@link SimulationResult} carrying the resolved `simulationTxs`, per-tx
47
- * `calls` (aligned 1:1 with `simulationTxs`), parsed `transfers` (each stamped with
48
- * `txIdx`), and the optional `tenderlyUrl`.
42
+ * `calls` (aligned 1:1 with `simulationTxs`), and parsed `transfers` (each stamped
43
+ * with `txIdx`).
49
44
  * @example
50
45
  * ```ts
51
46
  * import { simulate } from "@morpho-org/evm-simulation";
52
47
  *
53
48
  * const result = await simulate(
54
- * { chains: new Map([[1, { simulateV1Url: rpcUrl }]]) },
49
+ * {
50
+ * chains: new Map([
51
+ * [1, {
52
+ * tenderlyRpc: { rpcUrl: process.env.TENDERLY_RPC_URL! },
53
+ * simulateV1Url: process.env.MAINNET_RPC_URL,
54
+ * }],
55
+ * ]),
56
+ * },
55
57
  * {
56
58
  * chainId: 1,
57
59
  * transactions: [{ from: user, to: vaultAddress, data: encodedCalldata, value: 0n }],
@@ -60,9 +62,7 @@ const index_js_2 = require("./pipeline/index.js");
60
62
  * // result satisfies SimulationResult
61
63
  * ```
62
64
  */
63
- // biome-ignore lint/complexity/useMaxParams: TODO refactor to ≤2 params
64
- async function simulate(config, params, options = {}) {
65
- const { shareable = false } = options;
65
+ async function simulate(config, params) {
66
66
  (0, index_js_2.validateInput)(params);
67
67
  const simulationTxs = (0, index_js_2.buildSimulationTxs)(params);
68
68
  const result = await (0, index_js_2.executeSimulation)({
@@ -70,7 +70,6 @@ async function simulate(config, params, options = {}) {
70
70
  chainId: params.chainId,
71
71
  transactions: simulationTxs,
72
72
  blockNumber: params.blockNumber,
73
- shareable,
74
73
  });
75
74
  if (result.calls.length !== simulationTxs.length) {
76
75
  throw new errors_js_1.ExternalServiceError(`Backend returned ${result.calls.length} call result(s) for ${simulationTxs.length} transaction(s) — refusing to map transfers with mismatched lengths`);
@@ -85,6 +84,5 @@ async function simulate(config, params, options = {}) {
85
84
  simulationTxs,
86
85
  calls: result.calls,
87
86
  transfers,
88
- tenderlyUrl: result.tenderlyUrl,
89
87
  };
90
88
  }
@@ -1,36 +1,34 @@
1
1
  import type { Address, BlockTag, Hex } from "viem";
2
2
  /**
3
- * Credentials and routing for the Tenderly REST simulation backend.
4
- * Omit from `SimulationConfig.tenderlyRest` to disable the Tenderly path entirely.
3
+ * Credentials for the Tenderly Node Web3 Gateway. The `rpcUrl` is the
4
+ * chain-specific gateway URL with the access key embedded in the path,
5
+ * e.g. `https://mainnet.gateway.tenderly.co/{ACCESS_KEY}`.
5
6
  */
6
- export interface TenderlyRestConfig {
7
- /** e.g. "https://api.tenderly.co" */
8
- apiBaseUrl: string;
9
- accessToken: string;
10
- accountSlug: string;
11
- projectSlug: string;
12
- /** Chain IDs that Tenderly supports for simulation */
13
- supportedChainIds: Set<number>;
7
+ export interface TenderlyRpcConfig {
8
+ rpcUrl: string;
14
9
  }
15
10
  /**
16
- * Per-chain capabilities. Stored as a `Map` value in
17
- * `SimulationConfig.chains`, keyed by chain ID.
11
+ * Per-chain backend capabilities. Exactly one configuration shape per chain;
12
+ * the discriminated union enforces that **at least one** of `tenderlyRpc`
13
+ * (primary) or `simulateV1Url` (fallback) is supplied.
18
14
  */
19
- export interface ChainSimulationConfig {
20
- /** JSON-RPC URL supporting `eth_simulateV1`. undefined = not available for this chain. */
15
+ export type ChainSimulationConfig = {
16
+ /** Tenderly RPC config — `tenderly_simulateTransaction` / `tenderly_simulateBundle`. */
17
+ tenderlyRpc: TenderlyRpcConfig;
18
+ /** JSON-RPC URL supporting `eth_simulateV1`. Optional when Tenderly is set. */
21
19
  simulateV1Url?: string;
22
- }
20
+ } | {
21
+ tenderlyRpc?: TenderlyRpcConfig;
22
+ simulateV1Url: string;
23
+ };
23
24
  /**
24
25
  * Top-level configuration for `simulate`.
25
26
  *
26
- * At least one backend must be reachable for a given chainId — either Tenderly
27
- * (via `tenderlyRest` with that chainId in `TenderlyRestConfig.supportedChainIds`)
28
- * or `ChainSimulationConfig.simulateV1Url` in the `chains` map. Otherwise the
29
- * orchestrator throws `UnsupportedChainError`.
27
+ * Every chain entry must declare at least one backend (Tenderly RPC primary,
28
+ * `eth_simulateV1` fallback, or both). Calling `simulate` for a `chainId`
29
+ * missing from `chains` throws `UnsupportedChainError`.
30
30
  */
31
31
  export interface SimulationConfig {
32
- /** Tenderly REST API config. undefined = Tenderly not available. */
33
- tenderlyRest?: TenderlyRestConfig;
34
32
  /** Per-chain simulation capabilities. */
35
33
  chains: Map<number, ChainSimulationConfig>;
36
34
  logger?: SimulationLogger;
@@ -92,8 +90,6 @@ export interface Transfer {
92
90
  * status, returnData/gasUsed, and (Tenderly only) assetChanges per tx.
93
91
  * - `transfers[k].txIdx` indexes into `simulationTxs` to attribute each
94
92
  * transfer to its emitting transaction.
95
- * - `tenderlyUrl` is set only when `shareable: true` AND the Tenderly backend
96
- * ran successfully (not the `eth_simulateV1` fallback).
97
93
  */
98
94
  export interface SimulationResult {
99
95
  /** The full resolved transaction list (including prepended authorization txs). */
@@ -106,8 +102,6 @@ export interface SimulationResult {
106
102
  readonly calls: readonly SimulationCall[];
107
103
  /** Parsed ERC-20 / WETH9 transfers from the simulation. */
108
104
  readonly transfers: readonly Transfer[];
109
- /** Shareable Tenderly URL. Present only when `shareable: true` and Tenderly ran (not fallback). */
110
- readonly tenderlyUrl?: string;
111
105
  }
112
106
  /** Minimal structured logger the package calls for warnings and info. */
113
107
  export interface SimulationLogger {
@@ -132,11 +126,10 @@ export interface SimulateParams {
132
126
  */
133
127
  export interface RawSimulationResult {
134
128
  calls: RawCall[];
135
- tenderlyUrl?: string;
136
129
  }
137
130
  /**
138
131
  * Normalized EVM log emitted by a single simulated call. The shape is the
139
- * common subset both backends (`eth_simulateV1` via viem and Tenderly REST)
132
+ * common subset both backends (`eth_simulateV1` via viem and Tenderly RPC)
140
133
  * produce after schema validation. Returned indirectly via
141
134
  * `SimulationCall.logs` and consumed by the SDK's transfer parser.
142
135
  */
@@ -156,7 +149,7 @@ export interface RawCall {
156
149
  status: boolean;
157
150
  returnData: Hex;
158
151
  gasUsed: bigint;
159
- /** Tenderly-only `asset_changes` payload. Absent on `eth_simulateV1`. */
152
+ /** Tenderly-only `assetChanges` payload. Absent on `eth_simulateV1`. */
160
153
  assetChanges?: unknown;
161
154
  }
162
155
  /**
@@ -180,7 +173,7 @@ export interface SimulationCall {
180
173
  /** Gas used by this call (root frame). */
181
174
  readonly gasUsed: bigint;
182
175
  /**
183
- * Tenderly-only `asset_changes` payload for this tx. Opaque `unknown` —
176
+ * Tenderly-only `assetChanges` payload for this tx. Opaque `unknown` —
184
177
  * do not destructure without validation. Absent on `eth_simulateV1`.
185
178
  */
186
179
  readonly assetChanges?: unknown;
@@ -25,12 +25,6 @@ export declare class BlacklistViolationError extends SimulationPackageError {
25
25
  readonly code = "BLACKLIST_ERROR";
26
26
  constructor(message: string, assetChanges?: RetainedAsset[] | undefined);
27
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
28
  /** Tenderly or RPC service is down. Bypassable — user can proceed. */
35
29
  export declare class ExternalServiceError extends SimulationPackageError {
36
30
  readonly code = "EXTERNAL_SERVICE_ERROR";
package/lib/esm/errors.js CHANGED
@@ -28,15 +28,6 @@ export class BlacklistViolationError extends SimulationPackageError {
28
28
  this.assetChanges = assetChanges;
29
29
  }
30
30
  }
31
- /** Sanctioned or compromised address detected. Never bypassable. */
32
- export class AddressScreeningError extends SimulationPackageError {
33
- addresses;
34
- code = "SCREENING_ERROR";
35
- constructor(message, addresses) {
36
- super(message);
37
- this.addresses = addresses;
38
- }
39
- }
40
31
  /** Tenderly or RPC service is down. Bypassable — user can proceed. */
41
32
  export class ExternalServiceError extends SimulationPackageError {
42
33
  code = "EXTERNAL_SERVICE_ERROR";
@@ -1,4 +1,3 @@
1
- export { AddressScreeningError, BlacklistViolationError, ExternalServiceError, SimulationPackageError, SimulationRevertedError, SimulationValidationError, UnsupportedChainError, } from "./errors.js";
2
- export { screenAddresses } from "./screen-addresses/index.js";
1
+ export { BlacklistViolationError, ExternalServiceError, SimulationPackageError, SimulationRevertedError, SimulationValidationError, UnsupportedChainError, } from "./errors.js";
3
2
  export { simulate } from "./simulate/index.js";
4
- export type { ChainSimulationConfig, RawLog, SimulateParams, SimulationAuthorization, SimulationCall, SimulationConfig, SimulationLogger, SimulationResult, SimulationTransaction, TenderlyRestConfig, Transfer, } from "./types.js";
3
+ export type { ChainSimulationConfig, RawLog, SimulateParams, SimulationAuthorization, SimulationCall, SimulationConfig, SimulationLogger, SimulationResult, SimulationTransaction, TenderlyRpcConfig, Transfer, } from "./types.js";
package/lib/esm/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  // Public fns (via feature-folder barrels)
2
2
  // Errors (for instanceof checks by consumers)
3
- export { AddressScreeningError, BlacklistViolationError, ExternalServiceError, SimulationPackageError, SimulationRevertedError, SimulationValidationError, UnsupportedChainError, } from "./errors.js";
4
- export { screenAddresses } from "./screen-addresses/index.js";
3
+ export { BlacklistViolationError, ExternalServiceError, SimulationPackageError, SimulationRevertedError, SimulationValidationError, UnsupportedChainError, } from "./errors.js";
5
4
  export { simulate } from "./simulate/index.js";
@@ -1,2 +1,2 @@
1
1
  export { simulateV1 } from "./eth-simulate-v1.js";
2
- export { simulateTenderlyRest } from "./tenderly-rest.js";
2
+ export { simulateTenderlyRpc } from "./tenderly-rpc.js";
@@ -1,2 +1,2 @@
1
1
  export { simulateV1 } from "./eth-simulate-v1.js";
2
- export { simulateTenderlyRest } from "./tenderly-rest.js";
2
+ export { simulateTenderlyRpc } from "./tenderly-rpc.js";
@@ -0,0 +1,23 @@
1
+ import { type BlockTag } from "viem";
2
+ import type { RawSimulationResult, SimulationTransaction, TenderlyRpcConfig } from "../../types.js";
3
+ /**
4
+ * Simulate one or more transactions via Tenderly's Node Web3 Gateway.
5
+ * Dispatches to `tenderly_simulateTransaction` for a single call and
6
+ * `tenderly_simulateBundle` for a multi-call bundle.
7
+ *
8
+ * @param params.config - Tenderly RPC config (URL with access key embedded).
9
+ * @param params.transactions - Bundle to simulate, in execution order.
10
+ * @param params.blockNumber - Optional pinned block number or `BlockTag`. Defaults to `latest`.
11
+ * @param params.signal - Optional `AbortSignal` for cancellation / timeout.
12
+ * @returns A {@link RawSimulationResult} with one `RawCall` per input transaction.
13
+ * @throws {SimulationValidationError} when `transactions` is empty.
14
+ * @throws {SimulationRevertedError} when any simulated tx reports `status: false`.
15
+ * @throws {ExternalServiceError} on non-2xx response, JSON-RPC envelope error,
16
+ * schema-validation failure, or fetch-layer failure.
17
+ */
18
+ export declare function simulateTenderlyRpc(params: {
19
+ config: TenderlyRpcConfig;
20
+ transactions: SimulationTransaction[];
21
+ blockNumber?: bigint | BlockTag;
22
+ signal?: AbortSignal;
23
+ }): Promise<RawSimulationResult>;
@@ -0,0 +1,173 @@
1
+ import { isAddress, isHex, maxUint256, numberToHex, } from "viem";
2
+ import { z } from "zod";
3
+ import { ExternalServiceError, SimulationRevertedError, SimulationValidationError, } from "../../errors.js";
4
+ const addressSchema = z.custom((val) => typeof val === "string" && isAddress(val));
5
+ const hexSchema = z.custom((val) => typeof val === "string" && isHex(val));
6
+ const traceFrameSchema = z
7
+ .object({
8
+ output: hexSchema.optional(),
9
+ error: z.string().optional(),
10
+ errorReason: z.string().optional(),
11
+ })
12
+ .passthrough();
13
+ const logSchema = z
14
+ .object({
15
+ raw: z
16
+ .object({
17
+ address: addressSchema,
18
+ topics: z.array(hexSchema).optional(),
19
+ data: hexSchema.optional(),
20
+ })
21
+ .passthrough()
22
+ .optional(),
23
+ })
24
+ .passthrough();
25
+ const simResultSchema = z
26
+ .object({
27
+ status: z.boolean(),
28
+ gasUsed: hexSchema,
29
+ logs: z.array(logSchema).optional(),
30
+ trace: z.array(traceFrameSchema).optional(),
31
+ assetChanges: z.unknown().optional(),
32
+ error: z.string().optional(),
33
+ errorMessage: z.string().optional(),
34
+ })
35
+ .passthrough();
36
+ const rpcErrorSchema = z
37
+ .object({
38
+ code: z.number().optional(),
39
+ message: z.string(),
40
+ data: z.unknown().optional(),
41
+ })
42
+ .passthrough();
43
+ function rpcEnvelope(result) {
44
+ return z
45
+ .object({ result: result.optional(), error: rpcErrorSchema.optional() })
46
+ .passthrough();
47
+ }
48
+ const singleEnvelope = rpcEnvelope(simResultSchema);
49
+ const bundleEnvelope = rpcEnvelope(z.array(simResultSchema).min(1));
50
+ /**
51
+ * Simulate one or more transactions via Tenderly's Node Web3 Gateway.
52
+ * Dispatches to `tenderly_simulateTransaction` for a single call and
53
+ * `tenderly_simulateBundle` for a multi-call bundle.
54
+ *
55
+ * @param params.config - Tenderly RPC config (URL with access key embedded).
56
+ * @param params.transactions - Bundle to simulate, in execution order.
57
+ * @param params.blockNumber - Optional pinned block number or `BlockTag`. Defaults to `latest`.
58
+ * @param params.signal - Optional `AbortSignal` for cancellation / timeout.
59
+ * @returns A {@link RawSimulationResult} with one `RawCall` per input transaction.
60
+ * @throws {SimulationValidationError} when `transactions` is empty.
61
+ * @throws {SimulationRevertedError} when any simulated tx reports `status: false`.
62
+ * @throws {ExternalServiceError} on non-2xx response, JSON-RPC envelope error,
63
+ * schema-validation failure, or fetch-layer failure.
64
+ */
65
+ export async function simulateTenderlyRpc(params) {
66
+ const { config, transactions, blockNumber, signal } = params;
67
+ const firstTx = transactions[0];
68
+ if (!firstTx) {
69
+ throw new SimulationValidationError("At least one transaction is required", []);
70
+ }
71
+ const block = encodeBlock(blockNumber);
72
+ // Inflate sender ETH balance to avoid false "insufficient funds for gas"
73
+ // reverts on wallets low on native gas token — mirrors simulateV1.
74
+ const stateOverrides = buildStateOverrides(firstTx.from);
75
+ try {
76
+ if (transactions.length === 1) {
77
+ const json = await rpcRequest({
78
+ rpcUrl: config.rpcUrl,
79
+ method: "tenderly_simulateTransaction",
80
+ params: [buildCall(firstTx), block, stateOverrides],
81
+ signal,
82
+ });
83
+ const result = unwrapResult(singleEnvelope.parse(json));
84
+ return { calls: [toRawCall(result)] };
85
+ }
86
+ const json = await rpcRequest({
87
+ rpcUrl: config.rpcUrl,
88
+ method: "tenderly_simulateBundle",
89
+ params: [transactions.map(buildCall), block, stateOverrides],
90
+ signal,
91
+ });
92
+ const result = unwrapResult(bundleEnvelope.parse(json));
93
+ return { calls: result.map(toRawCall) };
94
+ }
95
+ catch (error) {
96
+ if (error instanceof SimulationRevertedError)
97
+ throw error;
98
+ if (error instanceof ExternalServiceError)
99
+ throw error;
100
+ throw new ExternalServiceError(`Tenderly RPC error: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
101
+ }
102
+ }
103
+ async function rpcRequest(params) {
104
+ const { rpcUrl, method, params: rpcParams, signal } = params;
105
+ const response = await fetch(rpcUrl, {
106
+ method: "POST",
107
+ headers: { "Content-Type": "application/json" },
108
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params: rpcParams }),
109
+ signal,
110
+ });
111
+ if (!response.ok) {
112
+ throw new ExternalServiceError(`Tenderly RPC returned ${response.status}: ${response.statusText}`);
113
+ }
114
+ return await response.json();
115
+ }
116
+ function unwrapResult(envelope) {
117
+ if (envelope.error) {
118
+ throw new ExternalServiceError(`Tenderly RPC error: ${envelope.error.message}`);
119
+ }
120
+ if (envelope.result === undefined) {
121
+ throw new ExternalServiceError("Tenderly RPC returned no result");
122
+ }
123
+ return envelope.result;
124
+ }
125
+ function buildCall(tx) {
126
+ return {
127
+ from: tx.from,
128
+ to: tx.to,
129
+ input: tx.data,
130
+ data: tx.data,
131
+ value: numberToHex(tx.value ?? 0n),
132
+ };
133
+ }
134
+ function buildStateOverrides(sender) {
135
+ return { [sender]: { balance: numberToHex(maxUint256) } };
136
+ }
137
+ function encodeBlock(blockNumber) {
138
+ if (blockNumber === undefined)
139
+ return "latest";
140
+ return typeof blockNumber === "bigint"
141
+ ? numberToHex(blockNumber)
142
+ : blockNumber;
143
+ }
144
+ function toRawCall(data) {
145
+ if (data.status !== true) {
146
+ // Tenderly RPC surfaces the revert reason on the trace frame; the
147
+ // top-level fields are checked as a defensive fallback.
148
+ const traceError = data.trace?.find((f) => f.errorReason || f.error);
149
+ const message = traceError?.errorReason ||
150
+ traceError?.error ||
151
+ data.errorMessage ||
152
+ data.error ||
153
+ "Transaction simulation reverted";
154
+ throw new SimulationRevertedError(message, data);
155
+ }
156
+ const logs = [];
157
+ for (const log of data.logs ?? []) {
158
+ if (log.raw) {
159
+ logs.push({
160
+ address: log.raw.address,
161
+ topics: log.raw.topics ?? [],
162
+ data: log.raw.data ?? "0x",
163
+ });
164
+ }
165
+ }
166
+ return {
167
+ logs,
168
+ status: true,
169
+ returnData: data.trace?.[0]?.output ?? "0x",
170
+ gasUsed: BigInt(data.gasUsed),
171
+ assetChanges: data.assetChanges,
172
+ };
173
+ }
@@ -141,7 +141,9 @@ export function parseTransfers(calls, logger) {
141
141
  catch (error) {
142
142
  // Safety net for anything that slips past the length checks above
143
143
  // (e.g., viem checksum throws on non-hex input). Loud enough to diagnose.
144
- warnMalformed(logger, log, error instanceof Error ? error.message : String(error));
144
+ warnMalformed(logger, log,
145
+ /* v8 ignore next: parser paths throw Error instances for valid RawLog shapes. */
146
+ error instanceof Error ? error.message : String(error));
145
147
  }
146
148
  }
147
149
  }
@@ -5,21 +5,17 @@ import type { RawSimulationResult, SimulationConfig, SimulationTransaction } fro
5
5
  *
6
6
  * Dispatches the transaction bundle to the available backend with a shared timeout
7
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.
8
+ * - If a Tenderly RPC URL is configured for the chain, it gets
9
+ * `TENDERLY_BUDGET_RATIO` (60%) of the timeout. On `ExternalServiceError`,
10
+ * falls back to `eth_simulateV1` with `max(remaining, FALLBACK_MIN_BUDGET_MS)`
11
+ * so the fallback still has a viable window when Tenderly eats its full slice.
12
+ * `SimulationRevertedError` (contract revert) propagates immediately without
13
+ * retry — a revert is a property of the bundle, not the backend.
14
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
15
  */
19
16
  export declare function executeSimulation(params: {
20
17
  config: SimulationConfig;
21
18
  chainId: number;
22
19
  transactions: SimulationTransaction[];
23
20
  blockNumber?: bigint | BlockTag;
24
- shareable: boolean;
25
21
  }): Promise<RawSimulationResult>;
@@ -1,5 +1,5 @@
1
1
  import { ExternalServiceError, UnsupportedChainError } from "../../errors.js";
2
- import { simulateTenderlyRest, simulateV1 } from "../backends/index.js";
2
+ import { simulateTenderlyRpc, simulateV1 } from "../backends/index.js";
3
3
  import { resolveChain } from "./resolve-chain.js";
4
4
  /** Total budget for a single `simulate()` call across both backends. */
5
5
  const DEFAULT_TIMEOUT_MS = 5000;
@@ -20,33 +20,27 @@ const FALLBACK_MIN_BUDGET_MS = 1500;
20
20
  *
21
21
  * Dispatches the transaction bundle to the available backend with a shared timeout
22
22
  * budget:
23
- * - If Tenderly is configured for the chain, it gets `TENDERLY_BUDGET_RATIO` (60%) of
24
- * the timeout. On `ExternalServiceError`, falls back to `eth_simulateV1` with
25
- * `max(remaining, FALLBACK_MIN_BUDGET_MS)` so the fallback still has a viable
26
- * window when Tenderly eats its full slice. `SimulationRevertedError` (contract
27
- * revert) propagates immediately without retry — a revert is a property of the
28
- * bundle, not the backend.
23
+ * - If a Tenderly RPC URL is configured for the chain, it gets
24
+ * `TENDERLY_BUDGET_RATIO` (60%) of the timeout. On `ExternalServiceError`,
25
+ * falls back to `eth_simulateV1` with `max(remaining, FALLBACK_MIN_BUDGET_MS)`
26
+ * so the fallback still has a viable window when Tenderly eats its full slice.
27
+ * `SimulationRevertedError` (contract revert) propagates immediately without
28
+ * retry — a revert is a property of the bundle, not the backend.
29
29
  * - If only `eth_simulateV1` is configured, it gets the full timeout.
30
- *
31
- * `shareable` is Tenderly-only: when true, the backend persists the simulation and
32
- * returns a shareable URL. `eth_simulateV1` ignores it (no persistence concept).
33
30
  */
34
31
  export async function executeSimulation(params) {
35
- const { config, chainId, transactions, blockNumber, shareable } = params;
32
+ const { config, chainId, transactions, blockNumber } = params;
36
33
  const chain = resolveChain(config, chainId);
37
34
  const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
38
35
  const deadline = Date.now() + timeoutMs;
39
- if (chain.tenderlySupported && config.tenderlyRest) {
36
+ if (chain.tenderlyRpc) {
40
37
  const tenderlyTimeout = Math.floor(timeoutMs * TENDERLY_BUDGET_RATIO);
41
38
  try {
42
- return await simulateTenderlyRest({
43
- config: config.tenderlyRest,
44
- chainId,
39
+ return await simulateTenderlyRpc({
40
+ config: chain.tenderlyRpc,
45
41
  transactions,
46
42
  blockNumber,
47
- shareable,
48
43
  signal: AbortSignal.timeout(tenderlyTimeout),
49
- logger: config.logger,
50
44
  });
51
45
  }
52
46
  catch (error) {
@@ -59,8 +53,6 @@ export async function executeSimulation(params) {
59
53
  });
60
54
  if (!chain.simulateV1Url)
61
55
  throw error;
62
- // Give the fallback a reasonable minimum even if Tenderly consumed the
63
- // whole budget — otherwise slow-Tenderly == total outage.
64
56
  const fallbackBudget = Math.max(deadline - Date.now(), FALLBACK_MIN_BUDGET_MS);
65
57
  return await simulateV1({
66
58
  rpcUrl: chain.simulateV1Url,
@@ -71,6 +63,7 @@ export async function executeSimulation(params) {
71
63
  });
72
64
  }
73
65
  }
66
+ /* v8 ignore next: resolveChain rejects this state before executeSimulation reaches the fallback path. */
74
67
  if (!chain.simulateV1Url) {
75
68
  throw new UnsupportedChainError(chainId);
76
69
  }
@@ -1,14 +1,15 @@
1
- import type { SimulationConfig } from "../../types.js";
1
+ import type { SimulationConfig, TenderlyRpcConfig } from "../../types.js";
2
2
  interface ChainCapability {
3
- tenderlySupported: boolean;
3
+ tenderlyRpc?: TenderlyRpcConfig;
4
4
  simulateV1Url?: string;
5
5
  }
6
6
  /**
7
7
  * Stage 2 of the simulate() pipeline.
8
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.
9
+ * Looks up the per-chain `ChainSimulationConfig` and exposes whichever
10
+ * backends were configured. Throws `UnsupportedChainError` when the chain is
11
+ * absent from the map or has neither backend wired (defensive — the type
12
+ * already enforces at least one at construction).
12
13
  */
13
14
  export declare function resolveChain(config: SimulationConfig, chainId: number): ChainCapability;
14
15
  export {};
@@ -2,15 +2,18 @@ import { UnsupportedChainError } from "../../errors.js";
2
2
  /**
3
3
  * Stage 2 of the simulate() pipeline.
4
4
  *
5
- * Determines which backend(s) are available for a chainId. Throws
6
- * `UnsupportedChainError` when neither Tenderly nor `eth_simulateV1` is reachable
7
- * for the requested chain.
5
+ * Looks up the per-chain `ChainSimulationConfig` and exposes whichever
6
+ * backends were configured. Throws `UnsupportedChainError` when the chain is
7
+ * absent from the map or has neither backend wired (defensive — the type
8
+ * already enforces at least one at construction).
8
9
  */
9
10
  export function resolveChain(config, chainId) {
10
- const tenderlySupported = config.tenderlyRest?.supportedChainIds.has(chainId) ?? false;
11
- const simulateV1Url = config.chains.get(chainId)?.simulateV1Url;
12
- if (!tenderlySupported && !simulateV1Url) {
11
+ const entry = config.chains.get(chainId);
12
+ if (!entry || (!entry.tenderlyRpc && !entry.simulateV1Url)) {
13
13
  throw new UnsupportedChainError(chainId);
14
14
  }
15
- return { tenderlySupported, simulateV1Url };
15
+ return {
16
+ tenderlyRpc: entry.tenderlyRpc,
17
+ simulateV1Url: entry.simulateV1Url,
18
+ };
16
19
  }