@morpho-org/evm-simulation 5.0.1 → 5.1.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 +1 -1
- package/lib/cjs/simulate/backends/eth-simulate-v1.d.ts +7 -2
- package/lib/cjs/simulate/backends/eth-simulate-v1.js +10 -3
- package/lib/cjs/simulate/backends/parse-response.d.ts +1 -0
- package/lib/cjs/simulate/backends/parse-response.js +4 -2
- package/lib/cjs/simulate/pipeline/resolve-chain.d.ts +5 -1
- package/lib/cjs/simulate/pipeline/resolve-chain.js +12 -1
- package/lib/cjs/simulate/run-simulation.js +2 -0
- package/lib/cjs/simulate/simulate.d.ts +7 -4
- package/lib/cjs/simulate/simulate.js +7 -4
- package/lib/cjs/types.d.ts +15 -0
- package/lib/esm/simulate/backends/eth-simulate-v1.d.ts +7 -2
- package/lib/esm/simulate/backends/eth-simulate-v1.js +10 -3
- package/lib/esm/simulate/backends/parse-response.d.ts +1 -0
- package/lib/esm/simulate/backends/parse-response.js +4 -2
- package/lib/esm/simulate/pipeline/resolve-chain.d.ts +5 -1
- package/lib/esm/simulate/pipeline/resolve-chain.js +13 -2
- package/lib/esm/simulate/run-simulation.js +2 -0
- package/lib/esm/simulate/simulate.d.ts +7 -4
- package/lib/esm/simulate/simulate.js +7 -4
- package/lib/esm/types.d.ts +15 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ try {
|
|
|
46
46
|
}
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
Every chain entry requires `simulateV1Url`, pointing to a JSON-RPC endpoint that supports `eth_simulateV1`. Requests send `validation: false` so gas is not charged. Monad (chain 143) is handled internally: its nodes reject `false`, so it sends `true` (its simulated block charges no gas either way), and it pins to the `finalized` block by default because its `latest` block is not final. On Monad, `gasUsed` reports the call's gas limit, not the gas consumed. Execution uses the full `timeoutMs` budget (default 5000 ms), with no retries or provider fallback. RPC failures, timeouts and reverts throw typed errors. The optional logger still reports parsing and retention warnings.
|
|
49
|
+
Every chain entry requires `simulateV1Url`, pointing to a JSON-RPC endpoint that supports `eth_simulateV1`. Requests send `validation: false` so gas is not charged. Monad (chain 143) is handled internally: its nodes reject `false`, so it sends `true` (its simulated block charges no gas either way), and it pins to the `finalized` block by default because its `latest` block is not final. On Monad, `gasUsed` reports the call's gas limit, not the gas consumed. Set `blockOverrides.gasLimit` on a chain entry to send that gas limit as the simulated block's `blockOverrides.gasLimit`; without it, no block override is sent. Set `parentHashCheck` on a chain entry to turn on or off the check that the simulated block's `parentHash` is the pinned block hash. It defaults to off on Stable (chain 988), whose nodes report a `parentHash` that never matches the pinned block, and on for every other chain. The block number, timestamp and reorg checks always apply. Execution uses the full `timeoutMs` budget (default 5000 ms), with no retries or provider fallback. RPC failures, timeouts and reverts throw typed errors. The optional logger still reports parsing and retention warnings.
|
|
50
50
|
|
|
51
51
|
Native-ETH movements are observed through `traceTransfers` logs on the simulated calls — no `stateOverrides` or helper contracts are injected.
|
|
52
52
|
|
|
@@ -29,7 +29,9 @@ export declare function assertEndpointChain(params: {
|
|
|
29
29
|
* synthesizes native-ETH moves as transfer logs, and `validation: false`
|
|
30
30
|
* (`true` on Monad, whose nodes reject `false` and charge no gas in the
|
|
31
31
|
* simulated block). Validation-off means gas is not charged, which is how
|
|
32
|
-
* gas is separated from economic effects.
|
|
32
|
+
* gas is separated from economic effects. When `blockGasLimit` is set,
|
|
33
|
+
* it is sent as the block's `blockOverrides.gasLimit`; otherwise no block
|
|
34
|
+
* override is sent.
|
|
33
35
|
* **No balance override is
|
|
34
36
|
* applied** — `value` transfers are funded by the sender's real native
|
|
35
37
|
* balance.
|
|
@@ -48,7 +50,8 @@ export declare function assertEndpointChain(params: {
|
|
|
48
50
|
* no fallback backend.
|
|
49
51
|
*
|
|
50
52
|
* @param params - Shared simulation client, execution plan, already-pinned
|
|
51
|
-
* state block,
|
|
53
|
+
* state block, the `validation` flag to send, the optional simulated
|
|
54
|
+
* block gas limit, and whether to check the successor `parentHash`.
|
|
52
55
|
* @returns Deep-frozen {@link SimulationExecution} — per-transaction call
|
|
53
56
|
* results and the resolved {@link ExecutionBlock}.
|
|
54
57
|
* @throws {ExternalServiceError} For transport failures, timeouts,
|
|
@@ -69,5 +72,7 @@ export declare function executePlan(params: {
|
|
|
69
72
|
plan: ExecutionPlan;
|
|
70
73
|
stateBlock: PinnedBlock;
|
|
71
74
|
validation: boolean;
|
|
75
|
+
blockGasLimit?: bigint | undefined;
|
|
76
|
+
parentHashCheck: boolean;
|
|
72
77
|
}): Promise<SimulationExecution>;
|
|
73
78
|
export {};
|
|
@@ -92,7 +92,9 @@ async function assertEndpointChain(params) {
|
|
|
92
92
|
* synthesizes native-ETH moves as transfer logs, and `validation: false`
|
|
93
93
|
* (`true` on Monad, whose nodes reject `false` and charge no gas in the
|
|
94
94
|
* simulated block). Validation-off means gas is not charged, which is how
|
|
95
|
-
* gas is separated from economic effects.
|
|
95
|
+
* gas is separated from economic effects. When `blockGasLimit` is set,
|
|
96
|
+
* it is sent as the block's `blockOverrides.gasLimit`; otherwise no block
|
|
97
|
+
* override is sent.
|
|
96
98
|
* **No balance override is
|
|
97
99
|
* applied** — `value` transfers are funded by the sender's real native
|
|
98
100
|
* balance.
|
|
@@ -111,7 +113,8 @@ async function assertEndpointChain(params) {
|
|
|
111
113
|
* no fallback backend.
|
|
112
114
|
*
|
|
113
115
|
* @param params - Shared simulation client, execution plan, already-pinned
|
|
114
|
-
* state block,
|
|
116
|
+
* state block, the `validation` flag to send, the optional simulated
|
|
117
|
+
* block gas limit, and whether to check the successor `parentHash`.
|
|
115
118
|
* @returns Deep-frozen {@link SimulationExecution} — per-transaction call
|
|
116
119
|
* results and the resolved {@link ExecutionBlock}.
|
|
117
120
|
* @throws {ExternalServiceError} For transport failures, timeouts,
|
|
@@ -128,13 +131,16 @@ async function assertEndpointChain(params) {
|
|
|
128
131
|
* @internal
|
|
129
132
|
*/
|
|
130
133
|
async function executePlan(params) {
|
|
131
|
-
const { client, plan, stateBlock, validation } = params;
|
|
134
|
+
const { client, plan, stateBlock, validation, blockGasLimit, parentHashCheck, } = params;
|
|
132
135
|
const response = await (0, exports.rpc)("eth_simulateV1", () => client.request({
|
|
133
136
|
method: "eth_simulateV1",
|
|
134
137
|
params: [
|
|
135
138
|
{
|
|
136
139
|
blockStateCalls: [
|
|
137
140
|
{
|
|
141
|
+
...(blockGasLimit !== undefined && {
|
|
142
|
+
blockOverrides: { gasLimit: (0, viem_1.numberToHex)(blockGasLimit) },
|
|
143
|
+
}),
|
|
138
144
|
calls: plan.calls.map((call) => ({
|
|
139
145
|
from: call.transaction.from,
|
|
140
146
|
to: call.transaction.to,
|
|
@@ -160,6 +166,7 @@ async function executePlan(params) {
|
|
|
160
166
|
stateBlockNumber: stateBlock.number,
|
|
161
167
|
stateBlockHash: stateBlock.hash,
|
|
162
168
|
stateBlockTimestamp: stateBlock.timestamp,
|
|
169
|
+
parentHashCheck,
|
|
163
170
|
});
|
|
164
171
|
// Reorg window: the pinned state block must still carry the same hash
|
|
165
172
|
// after simulation, or the result may describe a different chain tip.
|
|
@@ -57,8 +57,10 @@ function parseSimulationResponse(params) {
|
|
|
57
57
|
}
|
|
58
58
|
// Anvil re-hashes the pinned block, so only the geth-style successor can be
|
|
59
59
|
// pinned by hash: it must report a parentHash (geth always does) equal to
|
|
60
|
-
// the pinned state block hash.
|
|
61
|
-
|
|
60
|
+
// the pinned state block hash. With `parentHashCheck` off (Stable by
|
|
61
|
+
// default) the block number, timestamp and reorg checks still apply.
|
|
62
|
+
if (params.parentHashCheck &&
|
|
63
|
+
blockNumber === params.stateBlockNumber + 1n &&
|
|
62
64
|
block.parentHash !== params.stateBlockHash) {
|
|
63
65
|
throw new errors_js_1.InvalidSimulationResponseError(`eth_simulateV1 reported block ${blockNumber} whose parent ${String(block.parentHash)} is not the pinned state block hash ${params.stateBlockHash}; the node did not simulate on top of the pinned block.`, { context: errorContext });
|
|
64
66
|
}
|
|
@@ -4,6 +4,10 @@ import type { ChainSimulationConfig, SimulationConfig } from "../../types.js";
|
|
|
4
4
|
*
|
|
5
5
|
* Looks up the per-chain `ChainSimulationConfig`. Throws `UnsupportedChainError`
|
|
6
6
|
* when the chain is absent from the map or has no `simulateV1Url` (defensive —
|
|
7
|
-
* the type already requires it at construction).
|
|
7
|
+
* the type already requires it at construction). Throws
|
|
8
|
+
* `SimulationValidationError` when `blockOverrides.gasLimit` is set but is
|
|
9
|
+
* not a positive bigint, or `parentHashCheck` is set but is not a boolean, so a
|
|
10
|
+
* misconfiguration never surfaces as a bypassable
|
|
11
|
+
* `ExternalServiceError`.
|
|
8
12
|
*/
|
|
9
13
|
export declare function resolveChain(config: SimulationConfig, chainId: number): ChainSimulationConfig;
|
|
@@ -7,11 +7,22 @@ const errors_js_1 = require("../../errors.js");
|
|
|
7
7
|
*
|
|
8
8
|
* Looks up the per-chain `ChainSimulationConfig`. Throws `UnsupportedChainError`
|
|
9
9
|
* when the chain is absent from the map or has no `simulateV1Url` (defensive —
|
|
10
|
-
* the type already requires it at construction).
|
|
10
|
+
* the type already requires it at construction). Throws
|
|
11
|
+
* `SimulationValidationError` when `blockOverrides.gasLimit` is set but is
|
|
12
|
+
* not a positive bigint, or `parentHashCheck` is set but is not a boolean, so a
|
|
13
|
+
* misconfiguration never surfaces as a bypassable
|
|
14
|
+
* `ExternalServiceError`.
|
|
11
15
|
*/
|
|
12
16
|
function resolveChain(config, chainId) {
|
|
13
17
|
const entry = config.chains.get(chainId);
|
|
14
18
|
if (!entry?.simulateV1Url)
|
|
15
19
|
throw new errors_js_1.UnsupportedChainError(chainId);
|
|
20
|
+
const gasLimit = entry.blockOverrides?.gasLimit;
|
|
21
|
+
if (gasLimit !== undefined &&
|
|
22
|
+
(typeof gasLimit !== "bigint" || gasLimit <= 0n))
|
|
23
|
+
throw new errors_js_1.SimulationValidationError(`Chain ${chainId} blockOverrides.gasLimit must be a positive bigint, got "${String(gasLimit)}" (${typeof gasLimit}). Pass a bigint greater than 0n or omit gasLimit.`, ["blockOverrides.gasLimit: must be a positive bigint"]);
|
|
24
|
+
const parentHashCheck = entry.parentHashCheck;
|
|
25
|
+
if (parentHashCheck !== undefined && typeof parentHashCheck !== "boolean")
|
|
26
|
+
throw new errors_js_1.SimulationValidationError(`Chain ${chainId} parentHashCheck must be a boolean, got "${String(parentHashCheck)}" (${typeof parentHashCheck}). Pass true or false, or omit parentHashCheck.`, ["parentHashCheck: must be a boolean"]);
|
|
16
27
|
return entry;
|
|
17
28
|
}
|
|
@@ -91,6 +91,8 @@ async function runSimulation(params) {
|
|
|
91
91
|
plan,
|
|
92
92
|
stateBlock: pinnedBlock,
|
|
93
93
|
validation: monad,
|
|
94
|
+
blockGasLimit: chain.blockOverrides?.gasLimit,
|
|
95
|
+
parentHashCheck: chain.parentHashCheck ?? request.chainId !== blue_sdk_1.ChainId.StableMainnet,
|
|
94
96
|
});
|
|
95
97
|
const userCalls = execution.calls
|
|
96
98
|
.filter((call) => call.planned.type === "transaction")
|
|
@@ -37,8 +37,9 @@ import type { SimulationConfig } from "../types.js";
|
|
|
37
37
|
* separating gas from economic effects. Monad nodes reject `false`, so Monad
|
|
38
38
|
* simulations send `true`; its simulated block charges no gas either way.
|
|
39
39
|
*
|
|
40
|
-
* @param config - Required per-chain `eth_simulateV1` URL, optional
|
|
41
|
-
*
|
|
40
|
+
* @param config - Required per-chain `eth_simulateV1` URL, optional per-chain
|
|
41
|
+
* `blockOverrides.gasLimit` and `parentHashCheck`, optional logger, and the
|
|
42
|
+
* overall timeout budget.
|
|
42
43
|
* @param params - Per-call simulation input.
|
|
43
44
|
* @param params.chainId - Chain id the bundle targets; must match the endpoint.
|
|
44
45
|
* @param params.transactions - The bundle's transactions, in execution order.
|
|
@@ -52,7 +53,9 @@ import type { SimulationConfig } from "../types.js";
|
|
|
52
53
|
* final), resolved exactly once.
|
|
53
54
|
* @throws {SimulationValidationError} for invalid input (mixed senders, bad
|
|
54
55
|
* addresses, empty transactions, malformed authorizations, final-mode
|
|
55
|
-
* authorizations, malformed limits, unknown fields, a `"pending"` block tag,
|
|
56
|
+
* authorizations, malformed limits, unknown fields, a `"pending"` block tag, a
|
|
57
|
+
* `blockOverrides.gasLimit` that is not a positive bigint, a `parentHashCheck`
|
|
58
|
+
* that is not a boolean, or share quotes for
|
|
56
59
|
* `blueSupplyCollateral` / `blueWithdrawCollateral`).
|
|
57
60
|
* @throws {ConsumerLimitViolationError} when a declared `limits` bound is
|
|
58
61
|
* violated by the observed effects.
|
|
@@ -66,7 +69,7 @@ import type { SimulationConfig } from "../types.js";
|
|
|
66
69
|
* @throws {InvalidSimulationResponseError} when the node response cannot be
|
|
67
70
|
* trusted (bad shape, call-count mismatch, block that is neither the pinned
|
|
68
71
|
* state block nor its immediate successor, a successor with a mismatched
|
|
69
|
-
* `parentHash
|
|
72
|
+
* `parentHash` (unless `parentHashCheck` is off; off by default on Stable, chain 988), a block timestamp earlier than the pinned block's, a
|
|
70
73
|
* malformed per-call result, a quoted balance/position read whose non-empty
|
|
71
74
|
* return data cannot be decoded, a state-block hash that changed or a pinned
|
|
72
75
|
* block that vanished mid-flight, or an
|
|
@@ -39,8 +39,9 @@ const run_simulation_js_1 = require("./run-simulation.js");
|
|
|
39
39
|
* separating gas from economic effects. Monad nodes reject `false`, so Monad
|
|
40
40
|
* simulations send `true`; its simulated block charges no gas either way.
|
|
41
41
|
*
|
|
42
|
-
* @param config - Required per-chain `eth_simulateV1` URL, optional
|
|
43
|
-
*
|
|
42
|
+
* @param config - Required per-chain `eth_simulateV1` URL, optional per-chain
|
|
43
|
+
* `blockOverrides.gasLimit` and `parentHashCheck`, optional logger, and the
|
|
44
|
+
* overall timeout budget.
|
|
44
45
|
* @param params - Per-call simulation input.
|
|
45
46
|
* @param params.chainId - Chain id the bundle targets; must match the endpoint.
|
|
46
47
|
* @param params.transactions - The bundle's transactions, in execution order.
|
|
@@ -54,7 +55,9 @@ const run_simulation_js_1 = require("./run-simulation.js");
|
|
|
54
55
|
* final), resolved exactly once.
|
|
55
56
|
* @throws {SimulationValidationError} for invalid input (mixed senders, bad
|
|
56
57
|
* addresses, empty transactions, malformed authorizations, final-mode
|
|
57
|
-
* authorizations, malformed limits, unknown fields, a `"pending"` block tag,
|
|
58
|
+
* authorizations, malformed limits, unknown fields, a `"pending"` block tag, a
|
|
59
|
+
* `blockOverrides.gasLimit` that is not a positive bigint, a `parentHashCheck`
|
|
60
|
+
* that is not a boolean, or share quotes for
|
|
58
61
|
* `blueSupplyCollateral` / `blueWithdrawCollateral`).
|
|
59
62
|
* @throws {ConsumerLimitViolationError} when a declared `limits` bound is
|
|
60
63
|
* violated by the observed effects.
|
|
@@ -68,7 +71,7 @@ const run_simulation_js_1 = require("./run-simulation.js");
|
|
|
68
71
|
* @throws {InvalidSimulationResponseError} when the node response cannot be
|
|
69
72
|
* trusted (bad shape, call-count mismatch, block that is neither the pinned
|
|
70
73
|
* state block nor its immediate successor, a successor with a mismatched
|
|
71
|
-
* `parentHash
|
|
74
|
+
* `parentHash` (unless `parentHashCheck` is off; off by default on Stable, chain 988), a block timestamp earlier than the pinned block's, a
|
|
72
75
|
* malformed per-call result, a quoted balance/position read whose non-empty
|
|
73
76
|
* return data cannot be decoded, a state-block hash that changed or a pinned
|
|
74
77
|
* block that vanished mid-flight, or an
|
package/lib/cjs/types.d.ts
CHANGED
|
@@ -3,6 +3,21 @@ import type { Address, Hex } from "viem";
|
|
|
3
3
|
export interface ChainSimulationConfig {
|
|
4
4
|
/** JSON-RPC URL supporting `eth_simulateV1`. Required for every configured chain. */
|
|
5
5
|
readonly simulateV1Url: string;
|
|
6
|
+
/**
|
|
7
|
+
* Block overrides forwarded to `eth_simulateV1` as the simulated block's
|
|
8
|
+
* `blockOverrides`. Omitted fields leave the node's defaults untouched.
|
|
9
|
+
*/
|
|
10
|
+
readonly blockOverrides?: {
|
|
11
|
+
/** Gas limit of the simulated block. Omitted: no gas limit override is sent. */
|
|
12
|
+
readonly gasLimit?: bigint;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Whether a simulated successor block's `parentHash` must equal the pinned
|
|
16
|
+
* state block hash. Block number, timestamp and reorg checks always run.
|
|
17
|
+
* Omitted: on everywhere except Stable (988), whose nodes report a
|
|
18
|
+
* `parentHash` that never matches the pinned block.
|
|
19
|
+
*/
|
|
20
|
+
readonly parentHashCheck?: boolean;
|
|
6
21
|
}
|
|
7
22
|
/**
|
|
8
23
|
* Top-level configuration for `simulate`.
|
|
@@ -29,7 +29,9 @@ export declare function assertEndpointChain(params: {
|
|
|
29
29
|
* synthesizes native-ETH moves as transfer logs, and `validation: false`
|
|
30
30
|
* (`true` on Monad, whose nodes reject `false` and charge no gas in the
|
|
31
31
|
* simulated block). Validation-off means gas is not charged, which is how
|
|
32
|
-
* gas is separated from economic effects.
|
|
32
|
+
* gas is separated from economic effects. When `blockGasLimit` is set,
|
|
33
|
+
* it is sent as the block's `blockOverrides.gasLimit`; otherwise no block
|
|
34
|
+
* override is sent.
|
|
33
35
|
* **No balance override is
|
|
34
36
|
* applied** — `value` transfers are funded by the sender's real native
|
|
35
37
|
* balance.
|
|
@@ -48,7 +50,8 @@ export declare function assertEndpointChain(params: {
|
|
|
48
50
|
* no fallback backend.
|
|
49
51
|
*
|
|
50
52
|
* @param params - Shared simulation client, execution plan, already-pinned
|
|
51
|
-
* state block,
|
|
53
|
+
* state block, the `validation` flag to send, the optional simulated
|
|
54
|
+
* block gas limit, and whether to check the successor `parentHash`.
|
|
52
55
|
* @returns Deep-frozen {@link SimulationExecution} — per-transaction call
|
|
53
56
|
* results and the resolved {@link ExecutionBlock}.
|
|
54
57
|
* @throws {ExternalServiceError} For transport failures, timeouts,
|
|
@@ -69,5 +72,7 @@ export declare function executePlan(params: {
|
|
|
69
72
|
plan: ExecutionPlan;
|
|
70
73
|
stateBlock: PinnedBlock;
|
|
71
74
|
validation: boolean;
|
|
75
|
+
blockGasLimit?: bigint | undefined;
|
|
76
|
+
parentHashCheck: boolean;
|
|
72
77
|
}): Promise<SimulationExecution>;
|
|
73
78
|
export {};
|
|
@@ -86,7 +86,9 @@ export async function assertEndpointChain(params) {
|
|
|
86
86
|
* synthesizes native-ETH moves as transfer logs, and `validation: false`
|
|
87
87
|
* (`true` on Monad, whose nodes reject `false` and charge no gas in the
|
|
88
88
|
* simulated block). Validation-off means gas is not charged, which is how
|
|
89
|
-
* gas is separated from economic effects.
|
|
89
|
+
* gas is separated from economic effects. When `blockGasLimit` is set,
|
|
90
|
+
* it is sent as the block's `blockOverrides.gasLimit`; otherwise no block
|
|
91
|
+
* override is sent.
|
|
90
92
|
* **No balance override is
|
|
91
93
|
* applied** — `value` transfers are funded by the sender's real native
|
|
92
94
|
* balance.
|
|
@@ -105,7 +107,8 @@ export async function assertEndpointChain(params) {
|
|
|
105
107
|
* no fallback backend.
|
|
106
108
|
*
|
|
107
109
|
* @param params - Shared simulation client, execution plan, already-pinned
|
|
108
|
-
* state block,
|
|
110
|
+
* state block, the `validation` flag to send, the optional simulated
|
|
111
|
+
* block gas limit, and whether to check the successor `parentHash`.
|
|
109
112
|
* @returns Deep-frozen {@link SimulationExecution} — per-transaction call
|
|
110
113
|
* results and the resolved {@link ExecutionBlock}.
|
|
111
114
|
* @throws {ExternalServiceError} For transport failures, timeouts,
|
|
@@ -122,13 +125,16 @@ export async function assertEndpointChain(params) {
|
|
|
122
125
|
* @internal
|
|
123
126
|
*/
|
|
124
127
|
export async function executePlan(params) {
|
|
125
|
-
const { client, plan, stateBlock, validation } = params;
|
|
128
|
+
const { client, plan, stateBlock, validation, blockGasLimit, parentHashCheck, } = params;
|
|
126
129
|
const response = await rpc("eth_simulateV1", () => client.request({
|
|
127
130
|
method: "eth_simulateV1",
|
|
128
131
|
params: [
|
|
129
132
|
{
|
|
130
133
|
blockStateCalls: [
|
|
131
134
|
{
|
|
135
|
+
...(blockGasLimit !== undefined && {
|
|
136
|
+
blockOverrides: { gasLimit: numberToHex(blockGasLimit) },
|
|
137
|
+
}),
|
|
132
138
|
calls: plan.calls.map((call) => ({
|
|
133
139
|
from: call.transaction.from,
|
|
134
140
|
to: call.transaction.to,
|
|
@@ -154,6 +160,7 @@ export async function executePlan(params) {
|
|
|
154
160
|
stateBlockNumber: stateBlock.number,
|
|
155
161
|
stateBlockHash: stateBlock.hash,
|
|
156
162
|
stateBlockTimestamp: stateBlock.timestamp,
|
|
163
|
+
parentHashCheck,
|
|
157
164
|
});
|
|
158
165
|
// Reorg window: the pinned state block must still carry the same hash
|
|
159
166
|
// after simulation, or the result may describe a different chain tip.
|
|
@@ -54,8 +54,10 @@ export function parseSimulationResponse(params) {
|
|
|
54
54
|
}
|
|
55
55
|
// Anvil re-hashes the pinned block, so only the geth-style successor can be
|
|
56
56
|
// pinned by hash: it must report a parentHash (geth always does) equal to
|
|
57
|
-
// the pinned state block hash.
|
|
58
|
-
|
|
57
|
+
// the pinned state block hash. With `parentHashCheck` off (Stable by
|
|
58
|
+
// default) the block number, timestamp and reorg checks still apply.
|
|
59
|
+
if (params.parentHashCheck &&
|
|
60
|
+
blockNumber === params.stateBlockNumber + 1n &&
|
|
59
61
|
block.parentHash !== params.stateBlockHash) {
|
|
60
62
|
throw new InvalidSimulationResponseError(`eth_simulateV1 reported block ${blockNumber} whose parent ${String(block.parentHash)} is not the pinned state block hash ${params.stateBlockHash}; the node did not simulate on top of the pinned block.`, { context: errorContext });
|
|
61
63
|
}
|
|
@@ -4,6 +4,10 @@ import type { ChainSimulationConfig, SimulationConfig } from "../../types.js";
|
|
|
4
4
|
*
|
|
5
5
|
* Looks up the per-chain `ChainSimulationConfig`. Throws `UnsupportedChainError`
|
|
6
6
|
* when the chain is absent from the map or has no `simulateV1Url` (defensive —
|
|
7
|
-
* the type already requires it at construction).
|
|
7
|
+
* the type already requires it at construction). Throws
|
|
8
|
+
* `SimulationValidationError` when `blockOverrides.gasLimit` is set but is
|
|
9
|
+
* not a positive bigint, or `parentHashCheck` is set but is not a boolean, so a
|
|
10
|
+
* misconfiguration never surfaces as a bypassable
|
|
11
|
+
* `ExternalServiceError`.
|
|
8
12
|
*/
|
|
9
13
|
export declare function resolveChain(config: SimulationConfig, chainId: number): ChainSimulationConfig;
|
|
@@ -1,14 +1,25 @@
|
|
|
1
|
-
import { UnsupportedChainError } from "../../errors.js";
|
|
1
|
+
import { SimulationValidationError, UnsupportedChainError, } from "../../errors.js";
|
|
2
2
|
/**
|
|
3
3
|
* Stage 2 of the simulate() pipeline.
|
|
4
4
|
*
|
|
5
5
|
* Looks up the per-chain `ChainSimulationConfig`. Throws `UnsupportedChainError`
|
|
6
6
|
* when the chain is absent from the map or has no `simulateV1Url` (defensive —
|
|
7
|
-
* the type already requires it at construction).
|
|
7
|
+
* the type already requires it at construction). Throws
|
|
8
|
+
* `SimulationValidationError` when `blockOverrides.gasLimit` is set but is
|
|
9
|
+
* not a positive bigint, or `parentHashCheck` is set but is not a boolean, so a
|
|
10
|
+
* misconfiguration never surfaces as a bypassable
|
|
11
|
+
* `ExternalServiceError`.
|
|
8
12
|
*/
|
|
9
13
|
export function resolveChain(config, chainId) {
|
|
10
14
|
const entry = config.chains.get(chainId);
|
|
11
15
|
if (!entry?.simulateV1Url)
|
|
12
16
|
throw new UnsupportedChainError(chainId);
|
|
17
|
+
const gasLimit = entry.blockOverrides?.gasLimit;
|
|
18
|
+
if (gasLimit !== undefined &&
|
|
19
|
+
(typeof gasLimit !== "bigint" || gasLimit <= 0n))
|
|
20
|
+
throw new SimulationValidationError(`Chain ${chainId} blockOverrides.gasLimit must be a positive bigint, got "${String(gasLimit)}" (${typeof gasLimit}). Pass a bigint greater than 0n or omit gasLimit.`, ["blockOverrides.gasLimit: must be a positive bigint"]);
|
|
21
|
+
const parentHashCheck = entry.parentHashCheck;
|
|
22
|
+
if (parentHashCheck !== undefined && typeof parentHashCheck !== "boolean")
|
|
23
|
+
throw new SimulationValidationError(`Chain ${chainId} parentHashCheck must be a boolean, got "${String(parentHashCheck)}" (${typeof parentHashCheck}). Pass true or false, or omit parentHashCheck.`, ["parentHashCheck: must be a boolean"]);
|
|
13
24
|
return entry;
|
|
14
25
|
}
|
|
@@ -88,6 +88,8 @@ export async function runSimulation(params) {
|
|
|
88
88
|
plan,
|
|
89
89
|
stateBlock: pinnedBlock,
|
|
90
90
|
validation: monad,
|
|
91
|
+
blockGasLimit: chain.blockOverrides?.gasLimit,
|
|
92
|
+
parentHashCheck: chain.parentHashCheck ?? request.chainId !== ChainId.StableMainnet,
|
|
91
93
|
});
|
|
92
94
|
const userCalls = execution.calls
|
|
93
95
|
.filter((call) => call.planned.type === "transaction")
|
|
@@ -37,8 +37,9 @@ import type { SimulationConfig } from "../types.js";
|
|
|
37
37
|
* separating gas from economic effects. Monad nodes reject `false`, so Monad
|
|
38
38
|
* simulations send `true`; its simulated block charges no gas either way.
|
|
39
39
|
*
|
|
40
|
-
* @param config - Required per-chain `eth_simulateV1` URL, optional
|
|
41
|
-
*
|
|
40
|
+
* @param config - Required per-chain `eth_simulateV1` URL, optional per-chain
|
|
41
|
+
* `blockOverrides.gasLimit` and `parentHashCheck`, optional logger, and the
|
|
42
|
+
* overall timeout budget.
|
|
42
43
|
* @param params - Per-call simulation input.
|
|
43
44
|
* @param params.chainId - Chain id the bundle targets; must match the endpoint.
|
|
44
45
|
* @param params.transactions - The bundle's transactions, in execution order.
|
|
@@ -52,7 +53,9 @@ import type { SimulationConfig } from "../types.js";
|
|
|
52
53
|
* final), resolved exactly once.
|
|
53
54
|
* @throws {SimulationValidationError} for invalid input (mixed senders, bad
|
|
54
55
|
* addresses, empty transactions, malformed authorizations, final-mode
|
|
55
|
-
* authorizations, malformed limits, unknown fields, a `"pending"` block tag,
|
|
56
|
+
* authorizations, malformed limits, unknown fields, a `"pending"` block tag, a
|
|
57
|
+
* `blockOverrides.gasLimit` that is not a positive bigint, a `parentHashCheck`
|
|
58
|
+
* that is not a boolean, or share quotes for
|
|
56
59
|
* `blueSupplyCollateral` / `blueWithdrawCollateral`).
|
|
57
60
|
* @throws {ConsumerLimitViolationError} when a declared `limits` bound is
|
|
58
61
|
* violated by the observed effects.
|
|
@@ -66,7 +69,7 @@ import type { SimulationConfig } from "../types.js";
|
|
|
66
69
|
* @throws {InvalidSimulationResponseError} when the node response cannot be
|
|
67
70
|
* trusted (bad shape, call-count mismatch, block that is neither the pinned
|
|
68
71
|
* state block nor its immediate successor, a successor with a mismatched
|
|
69
|
-
* `parentHash
|
|
72
|
+
* `parentHash` (unless `parentHashCheck` is off; off by default on Stable, chain 988), a block timestamp earlier than the pinned block's, a
|
|
70
73
|
* malformed per-call result, a quoted balance/position read whose non-empty
|
|
71
74
|
* return data cannot be decoded, a state-block hash that changed or a pinned
|
|
72
75
|
* block that vanished mid-flight, or an
|
|
@@ -36,8 +36,9 @@ import { runSimulation } from "./run-simulation.js";
|
|
|
36
36
|
* separating gas from economic effects. Monad nodes reject `false`, so Monad
|
|
37
37
|
* simulations send `true`; its simulated block charges no gas either way.
|
|
38
38
|
*
|
|
39
|
-
* @param config - Required per-chain `eth_simulateV1` URL, optional
|
|
40
|
-
*
|
|
39
|
+
* @param config - Required per-chain `eth_simulateV1` URL, optional per-chain
|
|
40
|
+
* `blockOverrides.gasLimit` and `parentHashCheck`, optional logger, and the
|
|
41
|
+
* overall timeout budget.
|
|
41
42
|
* @param params - Per-call simulation input.
|
|
42
43
|
* @param params.chainId - Chain id the bundle targets; must match the endpoint.
|
|
43
44
|
* @param params.transactions - The bundle's transactions, in execution order.
|
|
@@ -51,7 +52,9 @@ import { runSimulation } from "./run-simulation.js";
|
|
|
51
52
|
* final), resolved exactly once.
|
|
52
53
|
* @throws {SimulationValidationError} for invalid input (mixed senders, bad
|
|
53
54
|
* addresses, empty transactions, malformed authorizations, final-mode
|
|
54
|
-
* authorizations, malformed limits, unknown fields, a `"pending"` block tag,
|
|
55
|
+
* authorizations, malformed limits, unknown fields, a `"pending"` block tag, a
|
|
56
|
+
* `blockOverrides.gasLimit` that is not a positive bigint, a `parentHashCheck`
|
|
57
|
+
* that is not a boolean, or share quotes for
|
|
55
58
|
* `blueSupplyCollateral` / `blueWithdrawCollateral`).
|
|
56
59
|
* @throws {ConsumerLimitViolationError} when a declared `limits` bound is
|
|
57
60
|
* violated by the observed effects.
|
|
@@ -65,7 +68,7 @@ import { runSimulation } from "./run-simulation.js";
|
|
|
65
68
|
* @throws {InvalidSimulationResponseError} when the node response cannot be
|
|
66
69
|
* trusted (bad shape, call-count mismatch, block that is neither the pinned
|
|
67
70
|
* state block nor its immediate successor, a successor with a mismatched
|
|
68
|
-
* `parentHash
|
|
71
|
+
* `parentHash` (unless `parentHashCheck` is off; off by default on Stable, chain 988), a block timestamp earlier than the pinned block's, a
|
|
69
72
|
* malformed per-call result, a quoted balance/position read whose non-empty
|
|
70
73
|
* return data cannot be decoded, a state-block hash that changed or a pinned
|
|
71
74
|
* block that vanished mid-flight, or an
|
package/lib/esm/types.d.ts
CHANGED
|
@@ -3,6 +3,21 @@ import type { Address, Hex } from "viem";
|
|
|
3
3
|
export interface ChainSimulationConfig {
|
|
4
4
|
/** JSON-RPC URL supporting `eth_simulateV1`. Required for every configured chain. */
|
|
5
5
|
readonly simulateV1Url: string;
|
|
6
|
+
/**
|
|
7
|
+
* Block overrides forwarded to `eth_simulateV1` as the simulated block's
|
|
8
|
+
* `blockOverrides`. Omitted fields leave the node's defaults untouched.
|
|
9
|
+
*/
|
|
10
|
+
readonly blockOverrides?: {
|
|
11
|
+
/** Gas limit of the simulated block. Omitted: no gas limit override is sent. */
|
|
12
|
+
readonly gasLimit?: bigint;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Whether a simulated successor block's `parentHash` must equal the pinned
|
|
16
|
+
* state block hash. Block number, timestamp and reorg checks always run.
|
|
17
|
+
* Omitted: on everywhere except Stable (988), whose nodes report a
|
|
18
|
+
* `parentHash` that never matches the pinned block.
|
|
19
|
+
*/
|
|
20
|
+
readonly parentHashCheck?: boolean;
|
|
6
21
|
}
|
|
7
22
|
/**
|
|
8
23
|
* Top-level configuration for `simulate`.
|
package/package.json
CHANGED