@morpho-org/evm-simulation 2.0.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -30
- package/lib/cjs/errors.d.ts +0 -6
- package/lib/cjs/errors.js +1 -11
- package/lib/cjs/index.d.ts +2 -3
- package/lib/cjs/index.js +3 -6
- package/lib/cjs/simulate/backends/index.d.ts +1 -1
- package/lib/cjs/simulate/backends/index.js +3 -3
- package/lib/cjs/simulate/backends/tenderly-rpc.d.ts +23 -0
- package/lib/cjs/simulate/backends/tenderly-rpc.js +176 -0
- package/lib/cjs/simulate/parsing/transfers.js +3 -1
- package/lib/cjs/simulate/pipeline/execute-simulation.d.ts +6 -10
- package/lib/cjs/simulate/pipeline/execute-simulation.js +11 -18
- package/lib/cjs/simulate/pipeline/resolve-chain.d.ts +6 -5
- package/lib/cjs/simulate/pipeline/resolve-chain.js +10 -7
- package/lib/cjs/simulate/simulate.d.ts +16 -21
- package/lib/cjs/simulate/simulate.js +16 -18
- package/lib/cjs/types.d.ts +22 -29
- package/lib/esm/errors.d.ts +0 -6
- package/lib/esm/errors.js +0 -9
- package/lib/esm/index.d.ts +2 -3
- package/lib/esm/index.js +1 -2
- package/lib/esm/simulate/backends/index.d.ts +1 -1
- package/lib/esm/simulate/backends/index.js +1 -1
- package/lib/esm/simulate/backends/tenderly-rpc.d.ts +23 -0
- package/lib/esm/simulate/backends/tenderly-rpc.js +173 -0
- package/lib/esm/simulate/parsing/transfers.js +3 -1
- package/lib/esm/simulate/pipeline/execute-simulation.d.ts +6 -10
- package/lib/esm/simulate/pipeline/execute-simulation.js +12 -19
- package/lib/esm/simulate/pipeline/resolve-chain.d.ts +6 -5
- package/lib/esm/simulate/pipeline/resolve-chain.js +10 -7
- package/lib/esm/simulate/simulate.d.ts +16 -21
- package/lib/esm/simulate/simulate.js +16 -18
- package/lib/esm/types.d.ts +22 -29
- package/package.json +3 -3
- package/src/index.ts +1 -3
- package/lib/cjs/screen-addresses/index.d.ts +0 -1
- package/lib/cjs/screen-addresses/index.js +0 -5
- package/lib/cjs/screen-addresses/sanctioned-addresses.d.ts +0 -11
- package/lib/cjs/screen-addresses/sanctioned-addresses.js +0 -77
- package/lib/cjs/screen-addresses/screen-addresses.d.ts +0 -50
- package/lib/cjs/screen-addresses/screen-addresses.js +0 -171
- package/lib/cjs/simulate/backends/tenderly-rest.d.ts +0 -20
- package/lib/cjs/simulate/backends/tenderly-rest.js +0 -279
- package/lib/esm/screen-addresses/index.d.ts +0 -1
- package/lib/esm/screen-addresses/index.js +0 -1
- package/lib/esm/screen-addresses/sanctioned-addresses.d.ts +0 -11
- package/lib/esm/screen-addresses/sanctioned-addresses.js +0 -74
- package/lib/esm/screen-addresses/screen-addresses.d.ts +0 -50
- package/lib/esm/screen-addresses/screen-addresses.js +0 -168
- package/lib/esm/simulate/backends/tenderly-rest.d.ts +0 -20
- package/lib/esm/simulate/backends/tenderly-rest.js +0 -276
|
@@ -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
|
|
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`
|
|
16
|
-
* - `simulationTxs` + `transfers` → server-side verification before broadcast
|
|
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:
|
|
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
|
|
48
|
-
* `txIdx`)
|
|
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
|
-
* {
|
|
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
|
-
|
|
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
|
}
|
package/lib/cjs/types.d.ts
CHANGED
|
@@ -1,36 +1,34 @@
|
|
|
1
1
|
import type { Address, BlockTag, Hex } from "viem";
|
|
2
2
|
/**
|
|
3
|
-
* Credentials
|
|
4
|
-
*
|
|
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
|
|
7
|
-
|
|
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.
|
|
17
|
-
*
|
|
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
|
|
20
|
-
/**
|
|
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
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
|
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 `
|
|
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 `
|
|
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;
|
package/lib/esm/errors.d.ts
CHANGED
|
@@ -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";
|
package/lib/esm/index.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
export {
|
|
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,
|
|
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 {
|
|
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 {
|
|
2
|
+
export { simulateTenderlyRpc } from "./tenderly-rpc.js";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { simulateV1 } from "./eth-simulate-v1.js";
|
|
2
|
-
export {
|
|
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,
|
|
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
|
|
9
|
-
* the timeout. On `ExternalServiceError`,
|
|
10
|
-
* `max(remaining, FALLBACK_MIN_BUDGET_MS)`
|
|
11
|
-
* window when Tenderly eats its full slice.
|
|
12
|
-
* revert) propagates immediately without
|
|
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 {
|
|
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
|
|
24
|
-
* the timeout. On `ExternalServiceError`,
|
|
25
|
-
* `max(remaining, FALLBACK_MIN_BUDGET_MS)`
|
|
26
|
-
* window when Tenderly eats its full slice.
|
|
27
|
-
* revert) propagates immediately without
|
|
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
|
|
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.
|
|
36
|
+
if (chain.tenderlyRpc) {
|
|
40
37
|
const tenderlyTimeout = Math.floor(timeoutMs * TENDERLY_BUDGET_RATIO);
|
|
41
38
|
try {
|
|
42
|
-
return await
|
|
43
|
-
config:
|
|
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
|
-
|
|
3
|
+
tenderlyRpc?: TenderlyRpcConfig;
|
|
4
4
|
simulateV1Url?: string;
|
|
5
5
|
}
|
|
6
6
|
/**
|
|
7
7
|
* Stage 2 of the simulate() pipeline.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
* `UnsupportedChainError` when
|
|
11
|
-
*
|
|
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
|
-
*
|
|
6
|
-
* `UnsupportedChainError` when
|
|
7
|
-
*
|
|
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
|
|
11
|
-
|
|
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 {
|
|
15
|
+
return {
|
|
16
|
+
tenderlyRpc: entry.tenderlyRpc,
|
|
17
|
+
simulateV1Url: entry.simulateV1Url,
|
|
18
|
+
};
|
|
16
19
|
}
|