@morpho-org/evm-simulation 2.0.2 → 4.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/asset-changes.d.ts +19 -0
- package/lib/cjs/simulate/asset-changes.js +47 -0
- package/lib/cjs/simulate/backends/eth-simulate-v1.d.ts +9 -0
- package/lib/cjs/simulate/backends/eth-simulate-v1.js +39 -1
- 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 +230 -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 +20 -23
- package/lib/cjs/simulate/simulate.js +21 -20
- package/lib/cjs/types.d.ts +58 -39
- 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/asset-changes.d.ts +19 -0
- package/lib/esm/simulate/asset-changes.js +44 -0
- package/lib/esm/simulate/backends/eth-simulate-v1.d.ts +9 -0
- package/lib/esm/simulate/backends/eth-simulate-v1.js +40 -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 +227 -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 +20 -23
- package/lib/esm/simulate/simulate.js +21 -20
- package/lib/esm/types.d.ts +58 -39
- package/package.json +7 -5
- package/src/index.ts +3 -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
|
@@ -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,227 @@
|
|
|
1
|
+
import { ethAddress, getAddress, isAddress, isHex, maxUint256, numberToHex, } from "viem";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { ExternalServiceError, SimulationRevertedError, SimulationValidationError, } from "../../errors.js";
|
|
4
|
+
import { groupAssetChanges } from "../asset-changes.js";
|
|
5
|
+
const addressSchema = z.custom((val) => typeof val === "string" && isAddress(val));
|
|
6
|
+
const hexSchema = z.custom((val) => typeof val === "string" && isHex(val));
|
|
7
|
+
const traceFrameSchema = z
|
|
8
|
+
.object({
|
|
9
|
+
output: hexSchema.optional(),
|
|
10
|
+
error: z.string().optional(),
|
|
11
|
+
errorReason: z.string().optional(),
|
|
12
|
+
})
|
|
13
|
+
.passthrough();
|
|
14
|
+
const logSchema = z
|
|
15
|
+
.object({
|
|
16
|
+
raw: z
|
|
17
|
+
.object({
|
|
18
|
+
address: addressSchema,
|
|
19
|
+
topics: z.array(hexSchema).optional(),
|
|
20
|
+
data: hexSchema.optional(),
|
|
21
|
+
})
|
|
22
|
+
.passthrough()
|
|
23
|
+
.optional(),
|
|
24
|
+
})
|
|
25
|
+
.passthrough();
|
|
26
|
+
const assetChangeSchema = z
|
|
27
|
+
.object({
|
|
28
|
+
assetInfo: z
|
|
29
|
+
.object({
|
|
30
|
+
contractAddress: addressSchema.optional(),
|
|
31
|
+
symbol: z.string().optional(),
|
|
32
|
+
decimals: z.number().optional(),
|
|
33
|
+
})
|
|
34
|
+
.passthrough(),
|
|
35
|
+
from: addressSchema.optional(),
|
|
36
|
+
to: addressSchema.optional(),
|
|
37
|
+
rawAmount: hexSchema,
|
|
38
|
+
})
|
|
39
|
+
.passthrough();
|
|
40
|
+
const simResultSchema = z
|
|
41
|
+
.object({
|
|
42
|
+
status: z.boolean(),
|
|
43
|
+
gasUsed: hexSchema,
|
|
44
|
+
logs: z.array(logSchema).optional(),
|
|
45
|
+
trace: z.array(traceFrameSchema).optional(),
|
|
46
|
+
assetChanges: z.array(assetChangeSchema).optional(),
|
|
47
|
+
error: z.string().optional(),
|
|
48
|
+
errorMessage: z.string().optional(),
|
|
49
|
+
})
|
|
50
|
+
.passthrough();
|
|
51
|
+
const rpcErrorSchema = z
|
|
52
|
+
.object({
|
|
53
|
+
code: z.number().optional(),
|
|
54
|
+
message: z.string(),
|
|
55
|
+
data: z.unknown().optional(),
|
|
56
|
+
})
|
|
57
|
+
.passthrough();
|
|
58
|
+
function rpcEnvelope(result) {
|
|
59
|
+
return z
|
|
60
|
+
.object({ result: result.optional(), error: rpcErrorSchema.optional() })
|
|
61
|
+
.passthrough();
|
|
62
|
+
}
|
|
63
|
+
const singleEnvelope = rpcEnvelope(simResultSchema);
|
|
64
|
+
const bundleEnvelope = rpcEnvelope(z.array(simResultSchema).min(1));
|
|
65
|
+
/**
|
|
66
|
+
* Simulate one or more transactions via Tenderly's Node Web3 Gateway.
|
|
67
|
+
* Dispatches to `tenderly_simulateTransaction` for a single call and
|
|
68
|
+
* `tenderly_simulateBundle` for a multi-call bundle.
|
|
69
|
+
*
|
|
70
|
+
* @param params.config - Tenderly RPC config (URL with access key embedded).
|
|
71
|
+
* @param params.transactions - Bundle to simulate, in execution order.
|
|
72
|
+
* @param params.blockNumber - Optional pinned block number or `BlockTag`. Defaults to `latest`.
|
|
73
|
+
* @param params.signal - Optional `AbortSignal` for cancellation / timeout.
|
|
74
|
+
* @returns A {@link RawSimulationResult} with one `RawCall` per input transaction.
|
|
75
|
+
* @throws {SimulationValidationError} when `transactions` is empty.
|
|
76
|
+
* @throws {SimulationRevertedError} when any simulated tx reports `status: false`.
|
|
77
|
+
* @throws {ExternalServiceError} on non-2xx response, JSON-RPC envelope error,
|
|
78
|
+
* schema-validation failure, or fetch-layer failure.
|
|
79
|
+
*/
|
|
80
|
+
export async function simulateTenderlyRpc(params) {
|
|
81
|
+
const { config, transactions, blockNumber, signal } = params;
|
|
82
|
+
const firstTx = transactions[0];
|
|
83
|
+
if (!firstTx) {
|
|
84
|
+
throw new SimulationValidationError("At least one transaction is required", []);
|
|
85
|
+
}
|
|
86
|
+
const block = encodeBlock(blockNumber);
|
|
87
|
+
// Inflate sender ETH balance to avoid false "insufficient funds for gas"
|
|
88
|
+
// reverts on wallets low on native gas token — mirrors simulateV1.
|
|
89
|
+
const stateOverrides = buildStateOverrides(firstTx.from);
|
|
90
|
+
try {
|
|
91
|
+
if (transactions.length === 1) {
|
|
92
|
+
const json = await rpcRequest({
|
|
93
|
+
rpcUrl: config.rpcUrl,
|
|
94
|
+
method: "tenderly_simulateTransaction",
|
|
95
|
+
params: [buildCall(firstTx), block, stateOverrides],
|
|
96
|
+
signal,
|
|
97
|
+
});
|
|
98
|
+
const result = unwrapResult(singleEnvelope.parse(json));
|
|
99
|
+
return {
|
|
100
|
+
calls: [toRawCall(result)],
|
|
101
|
+
assetChanges: toAssetChanges([result]),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const json = await rpcRequest({
|
|
105
|
+
rpcUrl: config.rpcUrl,
|
|
106
|
+
method: "tenderly_simulateBundle",
|
|
107
|
+
params: [transactions.map(buildCall), block, stateOverrides],
|
|
108
|
+
signal,
|
|
109
|
+
});
|
|
110
|
+
const results = unwrapResult(bundleEnvelope.parse(json));
|
|
111
|
+
return {
|
|
112
|
+
calls: results.map(toRawCall),
|
|
113
|
+
assetChanges: toAssetChanges(results),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (error instanceof SimulationRevertedError)
|
|
118
|
+
throw error;
|
|
119
|
+
if (error instanceof ExternalServiceError)
|
|
120
|
+
throw error;
|
|
121
|
+
throw new ExternalServiceError(`Tenderly RPC error: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function rpcRequest(params) {
|
|
125
|
+
const { rpcUrl, method, params: rpcParams, signal } = params;
|
|
126
|
+
const response = await fetch(rpcUrl, {
|
|
127
|
+
method: "POST",
|
|
128
|
+
headers: { "Content-Type": "application/json" },
|
|
129
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params: rpcParams }),
|
|
130
|
+
signal,
|
|
131
|
+
});
|
|
132
|
+
if (!response.ok) {
|
|
133
|
+
throw new ExternalServiceError(`Tenderly RPC returned ${response.status}: ${response.statusText}`);
|
|
134
|
+
}
|
|
135
|
+
return await response.json();
|
|
136
|
+
}
|
|
137
|
+
function unwrapResult(envelope) {
|
|
138
|
+
if (envelope.error) {
|
|
139
|
+
throw new ExternalServiceError(`Tenderly RPC error: ${envelope.error.message}`);
|
|
140
|
+
}
|
|
141
|
+
if (envelope.result === undefined) {
|
|
142
|
+
throw new ExternalServiceError("Tenderly RPC returned no result");
|
|
143
|
+
}
|
|
144
|
+
return envelope.result;
|
|
145
|
+
}
|
|
146
|
+
function buildCall(tx) {
|
|
147
|
+
return {
|
|
148
|
+
from: tx.from,
|
|
149
|
+
to: tx.to,
|
|
150
|
+
input: tx.data,
|
|
151
|
+
data: tx.data,
|
|
152
|
+
value: numberToHex(tx.value ?? 0n),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function buildStateOverrides(sender) {
|
|
156
|
+
return { [sender]: { balance: numberToHex(maxUint256) } };
|
|
157
|
+
}
|
|
158
|
+
function encodeBlock(blockNumber) {
|
|
159
|
+
if (blockNumber === undefined)
|
|
160
|
+
return "latest";
|
|
161
|
+
return typeof blockNumber === "bigint"
|
|
162
|
+
? numberToHex(blockNumber)
|
|
163
|
+
: blockNumber;
|
|
164
|
+
}
|
|
165
|
+
function toRawCall(data) {
|
|
166
|
+
if (data.status !== true) {
|
|
167
|
+
// Tenderly RPC surfaces the revert reason on the trace frame; the
|
|
168
|
+
// top-level fields are checked as a defensive fallback.
|
|
169
|
+
const traceError = data.trace?.find((f) => f.errorReason || f.error);
|
|
170
|
+
const message = traceError?.errorReason ||
|
|
171
|
+
traceError?.error ||
|
|
172
|
+
data.errorMessage ||
|
|
173
|
+
data.error ||
|
|
174
|
+
"Transaction simulation reverted";
|
|
175
|
+
throw new SimulationRevertedError(message, data);
|
|
176
|
+
}
|
|
177
|
+
const logs = [];
|
|
178
|
+
for (const log of data.logs ?? []) {
|
|
179
|
+
if (log.raw) {
|
|
180
|
+
logs.push({
|
|
181
|
+
address: log.raw.address,
|
|
182
|
+
topics: log.raw.topics ?? [],
|
|
183
|
+
data: log.raw.data ?? "0x",
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
logs,
|
|
189
|
+
status: true,
|
|
190
|
+
returnData: data.trace?.[0]?.output ?? "0x",
|
|
191
|
+
gasUsed: BigInt(data.gasUsed),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Reduce Tenderly's per-transfer asset changes to net per-token balance changes
|
|
196
|
+
* grouped by account. Both endpoints of each transfer are accounted (sender,
|
|
197
|
+
* counterparties, and the zero address for mints/burns).
|
|
198
|
+
*/
|
|
199
|
+
function toAssetChanges(results) {
|
|
200
|
+
const entries = [];
|
|
201
|
+
for (const result of results) {
|
|
202
|
+
for (const change of result.assetChanges ?? []) {
|
|
203
|
+
const amount = BigInt(change.rawAmount);
|
|
204
|
+
const token = change.assetInfo.contractAddress
|
|
205
|
+
? getAddress(change.assetInfo.contractAddress)
|
|
206
|
+
: ethAddress;
|
|
207
|
+
const { symbol, decimals } = change.assetInfo;
|
|
208
|
+
if (change.to)
|
|
209
|
+
entries.push({
|
|
210
|
+
account: change.to,
|
|
211
|
+
token,
|
|
212
|
+
diff: amount,
|
|
213
|
+
symbol,
|
|
214
|
+
decimals,
|
|
215
|
+
});
|
|
216
|
+
if (change.from)
|
|
217
|
+
entries.push({
|
|
218
|
+
account: change.from,
|
|
219
|
+
token,
|
|
220
|
+
diff: -amount,
|
|
221
|
+
symbol,
|
|
222
|
+
decimals,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return groupAssetChanges(entries);
|
|
227
|
+
}
|
|
@@ -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
|
}
|
|
@@ -3,20 +3,21 @@ import type { SimulateParams, SimulationConfig, SimulationResult } from "../type
|
|
|
3
3
|
* Simulate a bundle of EVM transactions.
|
|
4
4
|
*
|
|
5
5
|
* Validates input → resolves authorizations into prepended approve txs → runs the bundle
|
|
6
|
-
* through Tenderly
|
|
6
|
+
* through Tenderly RPC (primary) or `eth_simulateV1` (fallback) with a shared timeout
|
|
7
7
|
* budget → parses ERC20/WETH transfers from per-tx logs → asserts no funds are retained
|
|
8
8
|
* by `bundler3` → returns the full result set. The caller reads whichever fields they need:
|
|
9
9
|
*
|
|
10
|
-
* - `transfers`
|
|
11
|
-
* - `simulationTxs` + `transfers` → server-side verification before broadcast
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* - `transfers` → user-facing preview / server-side verification.
|
|
11
|
+
* - `simulationTxs` + `transfers` → server-side verification before broadcast.
|
|
12
|
+
* - `calls[i]` → per-tx raw backend output (`logs`, `status`, `returnData`, `gasUsed`).
|
|
13
|
+
* Aligned 1:1 with `simulationTxs[i]`.
|
|
14
|
+
* - `assetChanges` → net per-asset balance changes grouped by account (sender and
|
|
15
|
+
* counterparties) over the whole bundle, normalized to the same shape across backends.
|
|
15
16
|
* - `transfers[k].txIdx` → index into `simulationTxs` of the tx that emitted the
|
|
16
17
|
* underlying log; consumers map back via `simulationTxs[transfer.txIdx]`.
|
|
17
18
|
*
|
|
18
|
-
* @param config - Backend configuration:
|
|
19
|
-
* logger, and the overall timeout budget.
|
|
19
|
+
* @param config - Backend configuration: per-chain Tenderly RPC and/or `eth_simulateV1`
|
|
20
|
+
* URL, optional logger, and the overall timeout budget.
|
|
20
21
|
* @param params - Per-call simulation input.
|
|
21
22
|
* @param params.chainId - Chain id the bundle targets.
|
|
22
23
|
* @param params.transactions - The bundle's transactions, in execution order. All must share the
|
|
@@ -24,10 +25,6 @@ import type { SimulateParams, SimulationConfig, SimulationResult } from "../type
|
|
|
24
25
|
* @param params.authorizations - Optional token authorizations resolved into prepended approve
|
|
25
26
|
* transactions before the main bundle runs.
|
|
26
27
|
* @param params.blockNumber - Optional pinned block number or `BlockTag`. Defaults to `latest`.
|
|
27
|
-
* @param options - Per-call simulation options.
|
|
28
|
-
* @param options.shareable - When `true`, persists the simulation in Tenderly and returns a
|
|
29
|
-
* shareable `tenderlyUrl`. Honored only on the Tenderly backend, not on the `eth_simulateV1`
|
|
30
|
-
* fallback. Defaults to `false`.
|
|
31
28
|
* @throws {SimulationValidationError} for invalid input (mixed senders, bad addresses,
|
|
32
29
|
* empty transactions, malformed authorizations).
|
|
33
30
|
* @throws {UnsupportedChainError} when the chain is not configured for any backend.
|
|
@@ -39,14 +36,21 @@ import type { SimulateParams, SimulationConfig, SimulationResult } from "../type
|
|
|
39
36
|
* not match the resolved `simulationTxs` — refusing to map transfers with mismatched
|
|
40
37
|
* per-tx output.
|
|
41
38
|
* @returns A {@link SimulationResult} carrying the resolved `simulationTxs`, per-tx
|
|
42
|
-
* `calls` (aligned 1:1 with `simulationTxs`), parsed `transfers` (each stamped
|
|
43
|
-
* `txIdx`), and
|
|
39
|
+
* `calls` (aligned 1:1 with `simulationTxs`), parsed `transfers` (each stamped
|
|
40
|
+
* with `txIdx`), and per-account net `assetChanges`.
|
|
44
41
|
* @example
|
|
45
42
|
* ```ts
|
|
46
43
|
* import { simulate } from "@morpho-org/evm-simulation";
|
|
47
44
|
*
|
|
48
45
|
* const result = await simulate(
|
|
49
|
-
* {
|
|
46
|
+
* {
|
|
47
|
+
* chains: new Map([
|
|
48
|
+
* [1, {
|
|
49
|
+
* tenderlyRpc: { rpcUrl: process.env.TENDERLY_RPC_URL! },
|
|
50
|
+
* simulateV1Url: process.env.MAINNET_RPC_URL,
|
|
51
|
+
* }],
|
|
52
|
+
* ]),
|
|
53
|
+
* },
|
|
50
54
|
* {
|
|
51
55
|
* chainId: 1,
|
|
52
56
|
* transactions: [{ from: user, to: vaultAddress, data: encodedCalldata, value: 0n }],
|
|
@@ -55,11 +59,4 @@ import type { SimulateParams, SimulationConfig, SimulationResult } from "../type
|
|
|
55
59
|
* // result satisfies SimulationResult
|
|
56
60
|
* ```
|
|
57
61
|
*/
|
|
58
|
-
export declare function simulate(config: SimulationConfig, params: SimulateParams
|
|
59
|
-
/**
|
|
60
|
-
* When true, persist the simulation in Tenderly and return a shareable
|
|
61
|
-
* `tenderlyUrl` in the result. Only honored when the Tenderly backend runs
|
|
62
|
-
* (not the `eth_simulateV1` fallback). Defaults to `false`.
|
|
63
|
-
*/
|
|
64
|
-
shareable?: boolean;
|
|
65
|
-
}): Promise<SimulationResult>;
|
|
62
|
+
export declare function simulate(config: SimulationConfig, params: SimulateParams): Promise<SimulationResult>;
|
|
@@ -5,20 +5,21 @@ import { assertNoBundlerRetention, buildSimulationTxs, executeSimulation, valida
|
|
|
5
5
|
* Simulate a bundle of EVM transactions.
|
|
6
6
|
*
|
|
7
7
|
* Validates input → resolves authorizations into prepended approve txs → runs the bundle
|
|
8
|
-
* through Tenderly
|
|
8
|
+
* through Tenderly RPC (primary) or `eth_simulateV1` (fallback) with a shared timeout
|
|
9
9
|
* budget → parses ERC20/WETH transfers from per-tx logs → asserts no funds are retained
|
|
10
10
|
* by `bundler3` → returns the full result set. The caller reads whichever fields they need:
|
|
11
11
|
*
|
|
12
|
-
* - `transfers`
|
|
13
|
-
* - `simulationTxs` + `transfers` → server-side verification before broadcast
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
* - `transfers` → user-facing preview / server-side verification.
|
|
13
|
+
* - `simulationTxs` + `transfers` → server-side verification before broadcast.
|
|
14
|
+
* - `calls[i]` → per-tx raw backend output (`logs`, `status`, `returnData`, `gasUsed`).
|
|
15
|
+
* Aligned 1:1 with `simulationTxs[i]`.
|
|
16
|
+
* - `assetChanges` → net per-asset balance changes grouped by account (sender and
|
|
17
|
+
* counterparties) over the whole bundle, normalized to the same shape across backends.
|
|
17
18
|
* - `transfers[k].txIdx` → index into `simulationTxs` of the tx that emitted the
|
|
18
19
|
* underlying log; consumers map back via `simulationTxs[transfer.txIdx]`.
|
|
19
20
|
*
|
|
20
|
-
* @param config - Backend configuration:
|
|
21
|
-
* logger, and the overall timeout budget.
|
|
21
|
+
* @param config - Backend configuration: per-chain Tenderly RPC and/or `eth_simulateV1`
|
|
22
|
+
* URL, optional logger, and the overall timeout budget.
|
|
22
23
|
* @param params - Per-call simulation input.
|
|
23
24
|
* @param params.chainId - Chain id the bundle targets.
|
|
24
25
|
* @param params.transactions - The bundle's transactions, in execution order. All must share the
|
|
@@ -26,10 +27,6 @@ import { assertNoBundlerRetention, buildSimulationTxs, executeSimulation, valida
|
|
|
26
27
|
* @param params.authorizations - Optional token authorizations resolved into prepended approve
|
|
27
28
|
* transactions before the main bundle runs.
|
|
28
29
|
* @param params.blockNumber - Optional pinned block number or `BlockTag`. Defaults to `latest`.
|
|
29
|
-
* @param options - Per-call simulation options.
|
|
30
|
-
* @param options.shareable - When `true`, persists the simulation in Tenderly and returns a
|
|
31
|
-
* shareable `tenderlyUrl`. Honored only on the Tenderly backend, not on the `eth_simulateV1`
|
|
32
|
-
* fallback. Defaults to `false`.
|
|
33
30
|
* @throws {SimulationValidationError} for invalid input (mixed senders, bad addresses,
|
|
34
31
|
* empty transactions, malformed authorizations).
|
|
35
32
|
* @throws {UnsupportedChainError} when the chain is not configured for any backend.
|
|
@@ -41,14 +38,21 @@ import { assertNoBundlerRetention, buildSimulationTxs, executeSimulation, valida
|
|
|
41
38
|
* not match the resolved `simulationTxs` — refusing to map transfers with mismatched
|
|
42
39
|
* per-tx output.
|
|
43
40
|
* @returns A {@link SimulationResult} carrying the resolved `simulationTxs`, per-tx
|
|
44
|
-
* `calls` (aligned 1:1 with `simulationTxs`), parsed `transfers` (each stamped
|
|
45
|
-
* `txIdx`), and
|
|
41
|
+
* `calls` (aligned 1:1 with `simulationTxs`), parsed `transfers` (each stamped
|
|
42
|
+
* with `txIdx`), and per-account net `assetChanges`.
|
|
46
43
|
* @example
|
|
47
44
|
* ```ts
|
|
48
45
|
* import { simulate } from "@morpho-org/evm-simulation";
|
|
49
46
|
*
|
|
50
47
|
* const result = await simulate(
|
|
51
|
-
* {
|
|
48
|
+
* {
|
|
49
|
+
* chains: new Map([
|
|
50
|
+
* [1, {
|
|
51
|
+
* tenderlyRpc: { rpcUrl: process.env.TENDERLY_RPC_URL! },
|
|
52
|
+
* simulateV1Url: process.env.MAINNET_RPC_URL,
|
|
53
|
+
* }],
|
|
54
|
+
* ]),
|
|
55
|
+
* },
|
|
52
56
|
* {
|
|
53
57
|
* chainId: 1,
|
|
54
58
|
* transactions: [{ from: user, to: vaultAddress, data: encodedCalldata, value: 0n }],
|
|
@@ -57,9 +61,7 @@ import { assertNoBundlerRetention, buildSimulationTxs, executeSimulation, valida
|
|
|
57
61
|
* // result satisfies SimulationResult
|
|
58
62
|
* ```
|
|
59
63
|
*/
|
|
60
|
-
|
|
61
|
-
export async function simulate(config, params, options = {}) {
|
|
62
|
-
const { shareable = false } = options;
|
|
64
|
+
export async function simulate(config, params) {
|
|
63
65
|
validateInput(params);
|
|
64
66
|
const simulationTxs = buildSimulationTxs(params);
|
|
65
67
|
const result = await executeSimulation({
|
|
@@ -67,7 +69,6 @@ export async function simulate(config, params, options = {}) {
|
|
|
67
69
|
chainId: params.chainId,
|
|
68
70
|
transactions: simulationTxs,
|
|
69
71
|
blockNumber: params.blockNumber,
|
|
70
|
-
shareable,
|
|
71
72
|
});
|
|
72
73
|
if (result.calls.length !== simulationTxs.length) {
|
|
73
74
|
throw new ExternalServiceError(`Backend returned ${result.calls.length} call result(s) for ${simulationTxs.length} transaction(s) — refusing to map transfers with mismatched lengths`);
|
|
@@ -82,6 +83,6 @@ export async function simulate(config, params, options = {}) {
|
|
|
82
83
|
simulationTxs,
|
|
83
84
|
calls: result.calls,
|
|
84
85
|
transfers,
|
|
85
|
-
|
|
86
|
+
assetChanges: result.assetChanges,
|
|
86
87
|
};
|
|
87
88
|
}
|