@morpho-org/evm-simulation 5.0.1 → 6.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 +6 -4
- package/lib/cjs/errors.d.ts +17 -2
- package/lib/cjs/errors.js +19 -3
- package/lib/cjs/index.d.ts +2 -2
- package/lib/cjs/index.js +2 -1
- package/lib/cjs/params.d.ts +20 -1
- package/lib/cjs/requirements/requirements.js +2 -1
- package/lib/cjs/simulate/backends/eth-simulate-v1.d.ts +26 -34
- package/lib/cjs/simulate/backends/eth-simulate-v1.js +35 -74
- package/lib/cjs/simulate/backends/index.d.ts +1 -1
- package/lib/cjs/simulate/backends/index.js +1 -2
- 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/backends/resolve-pinned-block.d.ts +6 -12
- package/lib/cjs/simulate/backends/resolve-pinned-block.js +3 -4
- package/lib/cjs/simulate/pipeline/bundles-retention.d.ts +3 -3
- package/lib/cjs/simulate/pipeline/bundles-retention.js +6 -6
- 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/request/parse-request.d.ts +2 -1
- package/lib/cjs/simulate/request/parse-request.js +21 -0
- package/lib/cjs/simulate/run-simulation.js +9 -12
- package/lib/cjs/simulate/simulate.d.ts +30 -19
- package/lib/cjs/simulate/simulate.js +30 -19
- package/lib/cjs/types.d.ts +15 -0
- package/lib/esm/errors.d.ts +17 -2
- package/lib/esm/errors.js +17 -2
- package/lib/esm/index.d.ts +2 -2
- package/lib/esm/index.js +1 -1
- package/lib/esm/params.d.ts +20 -1
- package/lib/esm/requirements/requirements.js +2 -1
- package/lib/esm/simulate/backends/eth-simulate-v1.d.ts +26 -34
- package/lib/esm/simulate/backends/eth-simulate-v1.js +37 -75
- package/lib/esm/simulate/backends/index.d.ts +1 -1
- package/lib/esm/simulate/backends/index.js +1 -1
- 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/backends/resolve-pinned-block.d.ts +6 -12
- package/lib/esm/simulate/backends/resolve-pinned-block.js +3 -4
- package/lib/esm/simulate/pipeline/bundles-retention.d.ts +3 -3
- package/lib/esm/simulate/pipeline/bundles-retention.js +6 -6
- 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/request/parse-request.d.ts +2 -1
- package/lib/esm/simulate/request/parse-request.js +21 -0
- package/lib/esm/simulate/run-simulation.js +10 -13
- package/lib/esm/simulate/simulate.d.ts +30 -19
- package/lib/esm/simulate/simulate.js +30 -19
- package/lib/esm/types.d.ts +15 -0
- package/package.json +4 -4
|
@@ -1,63 +1,53 @@
|
|
|
1
|
-
import { type Client
|
|
2
|
-
import type {
|
|
1
|
+
import { type Client } from "viem";
|
|
2
|
+
import type { StateBlock } from "../../params.js";
|
|
3
3
|
import type { ExecutionPlan } from "../plan/plan-execution.js";
|
|
4
4
|
import type { SimulationExecution } from "./parse-response.js";
|
|
5
|
-
import type { PinnedBlock } from "./resolve-pinned-block.js";
|
|
6
5
|
/** The JSON-RPC methods this boundary calls. */
|
|
7
|
-
type RpcLabel = "eth_getBlock" | "
|
|
6
|
+
type RpcLabel = "eth_getBlock" | "eth_simulateV1";
|
|
8
7
|
/** Run one RPC call; anything thrown becomes a typed boundary error. @internal */
|
|
9
8
|
export declare const rpc: <T>(label: RpcLabel, call: () => Promise<T>) => Promise<T>;
|
|
10
|
-
/** Validate the endpoint identity through the same URL-safe RPC boundary. @internal */
|
|
11
|
-
export declare function assertEndpointChain(params: {
|
|
12
|
-
readonly client: Pick<PublicClient, "getChainId">;
|
|
13
|
-
readonly chainId: number;
|
|
14
|
-
readonly mode: SimulationMode;
|
|
15
|
-
readonly blockNumber?: bigint;
|
|
16
|
-
}): Promise<void>;
|
|
17
9
|
/**
|
|
18
10
|
* Execute an {@link ExecutionPlan} through a single `eth_simulateV1` call and
|
|
19
|
-
* collect the pinned execution.
|
|
11
|
+
* collect the pinned execution. This is the boundary's only RPC request.
|
|
20
12
|
*
|
|
21
|
-
* The caller
|
|
22
|
-
* block before this boundary. This function uses that same block for the
|
|
23
|
-
* simulation and reorg check under the shared abort/timeout budget.
|
|
24
|
-
*
|
|
25
|
-
* The boundary performs these steps:
|
|
13
|
+
* The caller resolves (or supplies) the state block before this boundary.
|
|
26
14
|
*
|
|
27
15
|
* 1. **`eth_simulateV1`** — one `blockStateCalls` entry carrying the planned
|
|
28
|
-
* calls with their per-call `from`, `traceTransfers: true`
|
|
29
|
-
* synthesizes native-ETH moves as transfer logs, and `validation: false`
|
|
16
|
+
* calls with their per-call `from` and `chainId`, `traceTransfers: true`
|
|
17
|
+
* so the node synthesizes native-ETH moves as transfer logs, and `validation: false`
|
|
30
18
|
* (`true` on Monad, whose nodes reject `false` and charge no gas in the
|
|
31
19
|
* simulated block). Validation-off means gas is not charged, which is how
|
|
32
|
-
* gas is separated from economic effects.
|
|
20
|
+
* gas is separated from economic effects. When `blockGasLimit` is set,
|
|
21
|
+
* it is sent as the block's `blockOverrides.gasLimit`; otherwise no block
|
|
22
|
+
* override is sent.
|
|
33
23
|
* **No balance override is
|
|
34
24
|
* applied** — `value` transfers are funded by the sender's real native
|
|
35
25
|
* balance.
|
|
36
|
-
* 2. **Response validation** — the
|
|
37
|
-
* re-fetch so revert/mismatch evidence already in hand surfaces instead
|
|
38
|
-
* of being downgraded to a bypassable {@link ExternalServiceError} by a
|
|
39
|
-
* failing re-fetch. The simulated block must be exactly
|
|
26
|
+
* 2. **Response validation** — the simulated block must be exactly
|
|
40
27
|
* `stateBlockNumber` or `stateBlockNumber + 1`: geth-style nodes report
|
|
41
28
|
* the former's successor while Anvil reports the pinned block itself.
|
|
42
29
|
* The result records whatever the node returns; consumers must read
|
|
43
30
|
* {@link ExecutionBlock.blockNumber} and never assume +1.
|
|
44
|
-
* 3. **Reorg check** — the pinned state block is re-fetched last to detect a reorg
|
|
45
|
-
* that swapped its hash mid-flight (`InvalidSimulationResponseError`).
|
|
46
31
|
*
|
|
47
32
|
* The endpoint must support `eth_simulateV1` with per-call `from`; there is
|
|
48
33
|
* no fallback backend.
|
|
49
34
|
*
|
|
50
|
-
* @param params - Shared simulation client, execution plan
|
|
51
|
-
* state block,
|
|
35
|
+
* @param params - Shared simulation client, execution plan (its request
|
|
36
|
+
* `chainId` is sent on every call), state block, the `validation` flag to
|
|
37
|
+
* send, the optional simulated block gas limit, and whether to check the
|
|
38
|
+
* successor `parentHash`.
|
|
52
39
|
* @returns Deep-frozen {@link SimulationExecution} — per-transaction call
|
|
53
40
|
* results and the resolved {@link ExecutionBlock}.
|
|
54
41
|
* @throws {ExternalServiceError} For transport failures, timeouts,
|
|
55
42
|
* or malformed JSON-RPC envelopes.
|
|
56
43
|
* @throws {InvalidSimulationResponseError} For a response that cannot be
|
|
57
|
-
* trusted (bad shape, call-count mismatch, block
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
44
|
+
* trusted (bad shape, call-count mismatch, block other than the state block
|
|
45
|
+
* or its successor, a block timestamp earlier than the state block's, or a
|
|
46
|
+
* per-call result that fails normalization).
|
|
47
|
+
* @throws {InvalidChainIdError} When the node rejects the request `chainId`
|
|
48
|
+
* because it serves another chain (a subclass of
|
|
49
|
+
* `InvalidSimulationResponseError` sharing its `name`/`code`;
|
|
50
|
+
* `instanceof InvalidChainIdError` tells them apart).
|
|
61
51
|
* @throws {MissingVerificationEvidenceError} When a planned state read fails.
|
|
62
52
|
* @throws {SimulationRevertedError} When a preparation or user transaction
|
|
63
53
|
* reverts or the node reports a bundle-level revert (code 3 / insufficient
|
|
@@ -65,9 +55,11 @@ export declare function assertEndpointChain(params: {
|
|
|
65
55
|
* @internal
|
|
66
56
|
*/
|
|
67
57
|
export declare function executePlan(params: {
|
|
68
|
-
client: Client
|
|
58
|
+
client: Client;
|
|
69
59
|
plan: ExecutionPlan;
|
|
70
|
-
stateBlock:
|
|
60
|
+
stateBlock: StateBlock;
|
|
71
61
|
validation: boolean;
|
|
62
|
+
blockGasLimit?: bigint | undefined;
|
|
63
|
+
parentHashCheck: boolean;
|
|
72
64
|
}): Promise<SimulationExecution>;
|
|
73
65
|
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { BaseError,
|
|
2
|
-
import { ExternalServiceError,
|
|
1
|
+
import { BaseError, ExecutionRevertedError, InsufficientFundsError, numberToHex, } from "viem";
|
|
2
|
+
import { ExternalServiceError, InvalidChainIdError, SimulationPackageError, SimulationRevertedError, } from "../../errors.js";
|
|
3
3
|
import { parseSimulationResponse } from "./parse-response.js";
|
|
4
4
|
/**
|
|
5
5
|
* Classify a node-level revert: viem's `ExecutionRevertedError`, or a raw
|
|
@@ -39,6 +39,15 @@ const toBoundaryError = (label, error) => {
|
|
|
39
39
|
reverted.cause = error;
|
|
40
40
|
return reverted;
|
|
41
41
|
}
|
|
42
|
+
// A wrong-chain endpoint is misconfiguration, not an outage: callers must
|
|
43
|
+
// not bypass it like an `ExternalServiceError`. Each node words the per-call
|
|
44
|
+
// `chainId` rejection differently: geth "chainId does not match node's",
|
|
45
|
+
// Anvil "invalid chain id for signer", Monad "Invalid chain ID: expected …".
|
|
46
|
+
if (label === "eth_simulateV1" &&
|
|
47
|
+
error instanceof Error &&
|
|
48
|
+
/chainId does not match node's|invalid chain id/i.test(error.message)) {
|
|
49
|
+
return new InvalidChainIdError(`eth_simulateV1 endpoint rejected the request chainId: ${safeMessage(error)}. Point SimulationConfig.chains at an RPC URL for this chain.`, { cause: error });
|
|
50
|
+
}
|
|
42
51
|
return new ExternalServiceError(`${label} error: ${safeMessage(error)}`, {
|
|
43
52
|
cause: error,
|
|
44
53
|
});
|
|
@@ -52,69 +61,48 @@ export const rpc = async (label, call) => {
|
|
|
52
61
|
throw toBoundaryError(label, error);
|
|
53
62
|
}
|
|
54
63
|
};
|
|
55
|
-
/** Validate the endpoint identity through the same URL-safe RPC boundary. @internal */
|
|
56
|
-
export async function assertEndpointChain(params) {
|
|
57
|
-
const { client, chainId, mode, blockNumber } = params;
|
|
58
|
-
const rpcChainId = await rpc("eth_chainId", () => client.getChainId());
|
|
59
|
-
if (rpcChainId === chainId)
|
|
60
|
-
return;
|
|
61
|
-
throw new InvalidSimulationResponseError(`The RPC configured for chain ${chainId} reports chain ${rpcChainId}. Fix SimulationConfig.chains.`, {
|
|
62
|
-
...(blockNumber !== undefined
|
|
63
|
-
? {
|
|
64
|
-
context: {
|
|
65
|
-
stage: "transport",
|
|
66
|
-
chainId,
|
|
67
|
-
mode,
|
|
68
|
-
blockNumber,
|
|
69
|
-
},
|
|
70
|
-
}
|
|
71
|
-
: {}),
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
64
|
/**
|
|
75
65
|
* Execute an {@link ExecutionPlan} through a single `eth_simulateV1` call and
|
|
76
|
-
* collect the pinned execution.
|
|
77
|
-
*
|
|
78
|
-
* The caller checks the endpoint chain identity and resolves the pinned state
|
|
79
|
-
* block before this boundary. This function uses that same block for the
|
|
80
|
-
* simulation and reorg check under the shared abort/timeout budget.
|
|
66
|
+
* collect the pinned execution. This is the boundary's only RPC request.
|
|
81
67
|
*
|
|
82
|
-
* The
|
|
68
|
+
* The caller resolves (or supplies) the state block before this boundary.
|
|
83
69
|
*
|
|
84
70
|
* 1. **`eth_simulateV1`** — one `blockStateCalls` entry carrying the planned
|
|
85
|
-
* calls with their per-call `from`, `traceTransfers: true`
|
|
86
|
-
* synthesizes native-ETH moves as transfer logs, and `validation: false`
|
|
71
|
+
* calls with their per-call `from` and `chainId`, `traceTransfers: true`
|
|
72
|
+
* so the node synthesizes native-ETH moves as transfer logs, and `validation: false`
|
|
87
73
|
* (`true` on Monad, whose nodes reject `false` and charge no gas in the
|
|
88
74
|
* simulated block). Validation-off means gas is not charged, which is how
|
|
89
|
-
* gas is separated from economic effects.
|
|
75
|
+
* gas is separated from economic effects. When `blockGasLimit` is set,
|
|
76
|
+
* it is sent as the block's `blockOverrides.gasLimit`; otherwise no block
|
|
77
|
+
* override is sent.
|
|
90
78
|
* **No balance override is
|
|
91
79
|
* applied** — `value` transfers are funded by the sender's real native
|
|
92
80
|
* balance.
|
|
93
|
-
* 2. **Response validation** — the
|
|
94
|
-
* re-fetch so revert/mismatch evidence already in hand surfaces instead
|
|
95
|
-
* of being downgraded to a bypassable {@link ExternalServiceError} by a
|
|
96
|
-
* failing re-fetch. The simulated block must be exactly
|
|
81
|
+
* 2. **Response validation** — the simulated block must be exactly
|
|
97
82
|
* `stateBlockNumber` or `stateBlockNumber + 1`: geth-style nodes report
|
|
98
83
|
* the former's successor while Anvil reports the pinned block itself.
|
|
99
84
|
* The result records whatever the node returns; consumers must read
|
|
100
85
|
* {@link ExecutionBlock.blockNumber} and never assume +1.
|
|
101
|
-
* 3. **Reorg check** — the pinned state block is re-fetched last to detect a reorg
|
|
102
|
-
* that swapped its hash mid-flight (`InvalidSimulationResponseError`).
|
|
103
86
|
*
|
|
104
87
|
* The endpoint must support `eth_simulateV1` with per-call `from`; there is
|
|
105
88
|
* no fallback backend.
|
|
106
89
|
*
|
|
107
|
-
* @param params - Shared simulation client, execution plan
|
|
108
|
-
* state block,
|
|
90
|
+
* @param params - Shared simulation client, execution plan (its request
|
|
91
|
+
* `chainId` is sent on every call), state block, the `validation` flag to
|
|
92
|
+
* send, the optional simulated block gas limit, and whether to check the
|
|
93
|
+
* successor `parentHash`.
|
|
109
94
|
* @returns Deep-frozen {@link SimulationExecution} — per-transaction call
|
|
110
95
|
* results and the resolved {@link ExecutionBlock}.
|
|
111
96
|
* @throws {ExternalServiceError} For transport failures, timeouts,
|
|
112
97
|
* or malformed JSON-RPC envelopes.
|
|
113
98
|
* @throws {InvalidSimulationResponseError} For a response that cannot be
|
|
114
|
-
* trusted (bad shape, call-count mismatch, block
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
99
|
+
* trusted (bad shape, call-count mismatch, block other than the state block
|
|
100
|
+
* or its successor, a block timestamp earlier than the state block's, or a
|
|
101
|
+
* per-call result that fails normalization).
|
|
102
|
+
* @throws {InvalidChainIdError} When the node rejects the request `chainId`
|
|
103
|
+
* because it serves another chain (a subclass of
|
|
104
|
+
* `InvalidSimulationResponseError` sharing its `name`/`code`;
|
|
105
|
+
* `instanceof InvalidChainIdError` tells them apart).
|
|
118
106
|
* @throws {MissingVerificationEvidenceError} When a planned state read fails.
|
|
119
107
|
* @throws {SimulationRevertedError} When a preparation or user transaction
|
|
120
108
|
* reverts or the node reports a bundle-level revert (code 3 / insufficient
|
|
@@ -122,14 +110,18 @@ export async function assertEndpointChain(params) {
|
|
|
122
110
|
* @internal
|
|
123
111
|
*/
|
|
124
112
|
export async function executePlan(params) {
|
|
125
|
-
const { client, plan, stateBlock, validation } = params;
|
|
113
|
+
const { client, plan, stateBlock, validation, blockGasLimit, parentHashCheck, } = params;
|
|
126
114
|
const response = await rpc("eth_simulateV1", () => client.request({
|
|
127
115
|
method: "eth_simulateV1",
|
|
128
116
|
params: [
|
|
129
117
|
{
|
|
130
118
|
blockStateCalls: [
|
|
131
119
|
{
|
|
120
|
+
...(blockGasLimit !== undefined && {
|
|
121
|
+
blockOverrides: { gasLimit: numberToHex(blockGasLimit) },
|
|
122
|
+
}),
|
|
132
123
|
calls: plan.calls.map((call) => ({
|
|
124
|
+
chainId: numberToHex(plan.request.chainId),
|
|
133
125
|
from: call.transaction.from,
|
|
134
126
|
to: call.transaction.to,
|
|
135
127
|
data: call.transaction.data,
|
|
@@ -143,42 +135,12 @@ export async function executePlan(params) {
|
|
|
143
135
|
numberToHex(stateBlock.number),
|
|
144
136
|
],
|
|
145
137
|
}));
|
|
146
|
-
|
|
147
|
-
// reorg re-fetch so evidence already in hand reaches the caller as
|
|
148
|
-
// InvalidSimulationResponseError / SimulationRevertedError instead of
|
|
149
|
-
// being downgraded to a bypassable ExternalServiceError by a failing
|
|
150
|
-
// re-fetch.
|
|
151
|
-
const execution = parseSimulationResponse({
|
|
138
|
+
return parseSimulationResponse({
|
|
152
139
|
plan,
|
|
153
140
|
blocks: response,
|
|
154
141
|
stateBlockNumber: stateBlock.number,
|
|
155
142
|
stateBlockHash: stateBlock.hash,
|
|
156
143
|
stateBlockTimestamp: stateBlock.timestamp,
|
|
144
|
+
parentHashCheck,
|
|
157
145
|
});
|
|
158
|
-
// Reorg window: the pinned state block must still carry the same hash
|
|
159
|
-
// after simulation, or the result may describe a different chain tip.
|
|
160
|
-
const stateBlockAfter = await rpc("eth_getBlock", () => client.getBlock({ blockNumber: stateBlock.number })).catch((error) => {
|
|
161
|
-
if (error instanceof ExternalServiceError &&
|
|
162
|
-
error.cause instanceof BlockNotFoundError)
|
|
163
|
-
return error;
|
|
164
|
-
throw error;
|
|
165
|
-
});
|
|
166
|
-
if (stateBlockAfter instanceof ExternalServiceError ||
|
|
167
|
-
stateBlockAfter.hash !== stateBlock.hash) {
|
|
168
|
-
const after = stateBlockAfter instanceof ExternalServiceError
|
|
169
|
-
? "unavailable"
|
|
170
|
-
: stateBlockAfter.hash;
|
|
171
|
-
throw new InvalidSimulationResponseError(`State block ${stateBlock.number} hash changed during simulation (reorg): ${stateBlock.hash} became ${after}. Re-submit the simulation.`, {
|
|
172
|
-
context: {
|
|
173
|
-
stage: "transport",
|
|
174
|
-
chainId: plan.request.chainId,
|
|
175
|
-
mode: plan.request.mode,
|
|
176
|
-
blockNumber: stateBlock.number,
|
|
177
|
-
},
|
|
178
|
-
...(stateBlockAfter instanceof ExternalServiceError && {
|
|
179
|
-
cause: stateBlockAfter.cause,
|
|
180
|
-
}),
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
return execution;
|
|
184
146
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { executePlan } from "./eth-simulate-v1.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { executePlan } from "./eth-simulate-v1.js";
|
|
@@ -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 and timestamp 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
|
}
|
|
@@ -1,16 +1,10 @@
|
|
|
1
|
-
import type { BlockTag, Client
|
|
2
|
-
|
|
3
|
-
export interface PinnedBlock {
|
|
4
|
-
readonly number: bigint;
|
|
5
|
-
readonly hash: Hex;
|
|
6
|
-
readonly timestamp: bigint;
|
|
7
|
-
}
|
|
1
|
+
import type { BlockTag, Client } from "viem";
|
|
2
|
+
import type { StateBlock } from "../../params.js";
|
|
8
3
|
/**
|
|
9
4
|
* Resolve the requested state block exactly once so `latest` cannot drift.
|
|
10
5
|
*
|
|
11
|
-
* Every downstream read and the `eth_simulateV1` call pin this number
|
|
12
|
-
*
|
|
13
|
-
* swapped the block mid-flight.
|
|
6
|
+
* Every downstream read and the `eth_simulateV1` call pin this number. Skipped
|
|
7
|
+
* when the caller supplies `SimulateParams.block`.
|
|
14
8
|
*
|
|
15
9
|
* @param params - Resolution parameters.
|
|
16
10
|
* @param params.client - Shared simulation client (created by
|
|
@@ -19,7 +13,7 @@ export interface PinnedBlock {
|
|
|
19
13
|
* `latest`.
|
|
20
14
|
* @param params.signal - Optional pipeline abort signal; checked around the
|
|
21
15
|
* request so an aborted pipeline never produces a pin.
|
|
22
|
-
* @returns The resolved {@link
|
|
16
|
+
* @returns The resolved {@link StateBlock}.
|
|
23
17
|
* @throws {ExternalServiceError} For transport failures, timeouts, or an
|
|
24
18
|
* aborted signal.
|
|
25
19
|
* @throws {InvalidSimulationResponseError} When the node returns a block
|
|
@@ -30,4 +24,4 @@ export declare function resolvePinnedBlock(params: {
|
|
|
30
24
|
readonly client: Client;
|
|
31
25
|
readonly blockNumber?: bigint | BlockTag;
|
|
32
26
|
readonly signal?: AbortSignal;
|
|
33
|
-
}): Promise<
|
|
27
|
+
}): Promise<StateBlock>;
|
|
@@ -4,9 +4,8 @@ import { rpc } from "./eth-simulate-v1.js";
|
|
|
4
4
|
/**
|
|
5
5
|
* Resolve the requested state block exactly once so `latest` cannot drift.
|
|
6
6
|
*
|
|
7
|
-
* Every downstream read and the `eth_simulateV1` call pin this number
|
|
8
|
-
*
|
|
9
|
-
* swapped the block mid-flight.
|
|
7
|
+
* Every downstream read and the `eth_simulateV1` call pin this number. Skipped
|
|
8
|
+
* when the caller supplies `SimulateParams.block`.
|
|
10
9
|
*
|
|
11
10
|
* @param params - Resolution parameters.
|
|
12
11
|
* @param params.client - Shared simulation client (created by
|
|
@@ -15,7 +14,7 @@ import { rpc } from "./eth-simulate-v1.js";
|
|
|
15
14
|
* `latest`.
|
|
16
15
|
* @param params.signal - Optional pipeline abort signal; checked around the
|
|
17
16
|
* request so an aborted pipeline never produces a pin.
|
|
18
|
-
* @returns The resolved {@link
|
|
17
|
+
* @returns The resolved {@link StateBlock}.
|
|
19
18
|
* @throws {ExternalServiceError} For transport failures, timeouts, or an
|
|
20
19
|
* aborted signal.
|
|
21
20
|
* @throws {InvalidSimulationResponseError} When the node returns a block
|
|
@@ -8,7 +8,7 @@ interface AssertNoBundlesRetentionParams {
|
|
|
8
8
|
/**
|
|
9
9
|
* Assert that no value is retained by standalone `bundles` periphery contracts
|
|
10
10
|
* (`VaultExitBundlesV1`, `VaultBundlesV1`, `BlueBundlesV1`) or by
|
|
11
|
-
* `
|
|
11
|
+
* `MidnightBundlesV2` (`midnightBundlesV2`).
|
|
12
12
|
*
|
|
13
13
|
* Uses net flow (inbound minus outbound) per (restricted address, token) pair.
|
|
14
14
|
* These contracts legitimately receive tokens as an intermediary (user →
|
|
@@ -33,7 +33,7 @@ interface AssertNoBundlesRetentionParams {
|
|
|
33
33
|
*
|
|
34
34
|
* @internal Internal pipeline stage, composed by `simulate`; not part of the
|
|
35
35
|
* package's public API (only re-exported from the internal pipeline barrel).
|
|
36
|
-
* @param params.chainId - Chain whose blue-sdk `bundles` and `
|
|
36
|
+
* @param params.chainId - Chain whose blue-sdk `bundles` and `midnightBundlesV2`
|
|
37
37
|
* registry entries define the restricted address set. Chains cataloging
|
|
38
38
|
* neither are skipped (a `logger.warn` records the skip).
|
|
39
39
|
* @param params.transfers - Parsed ERC20 / WETH9 transfer flows (plus the
|
|
@@ -45,7 +45,7 @@ interface AssertNoBundlesRetentionParams {
|
|
|
45
45
|
* @returns Nothing. Returns silently when no restricted contract retains value
|
|
46
46
|
* above `DUST_THRESHOLD`; otherwise throws.
|
|
47
47
|
* @throws {BlacklistViolationError} when net inbound flow to a restricted
|
|
48
|
-
* `bundles` or `
|
|
48
|
+
* `bundles` or `midnightBundlesV2` contract exceeds `DUST_THRESHOLD` for any
|
|
49
49
|
* token.
|
|
50
50
|
*/
|
|
51
51
|
export declare function assertNoBundlesRetention(params: AssertNoBundlesRetentionParams): void;
|
|
@@ -15,7 +15,7 @@ const DUST_THRESHOLD = 100n;
|
|
|
15
15
|
/**
|
|
16
16
|
* Assert that no value is retained by standalone `bundles` periphery contracts
|
|
17
17
|
* (`VaultExitBundlesV1`, `VaultBundlesV1`, `BlueBundlesV1`) or by
|
|
18
|
-
* `
|
|
18
|
+
* `MidnightBundlesV2` (`midnightBundlesV2`).
|
|
19
19
|
*
|
|
20
20
|
* Uses net flow (inbound minus outbound) per (restricted address, token) pair.
|
|
21
21
|
* These contracts legitimately receive tokens as an intermediary (user →
|
|
@@ -40,7 +40,7 @@ const DUST_THRESHOLD = 100n;
|
|
|
40
40
|
*
|
|
41
41
|
* @internal Internal pipeline stage, composed by `simulate`; not part of the
|
|
42
42
|
* package's public API (only re-exported from the internal pipeline barrel).
|
|
43
|
-
* @param params.chainId - Chain whose blue-sdk `bundles` and `
|
|
43
|
+
* @param params.chainId - Chain whose blue-sdk `bundles` and `midnightBundlesV2`
|
|
44
44
|
* registry entries define the restricted address set. Chains cataloging
|
|
45
45
|
* neither are skipped (a `logger.warn` records the skip).
|
|
46
46
|
* @param params.transfers - Parsed ERC20 / WETH9 transfer flows (plus the
|
|
@@ -52,12 +52,12 @@ const DUST_THRESHOLD = 100n;
|
|
|
52
52
|
* @returns Nothing. Returns silently when no restricted contract retains value
|
|
53
53
|
* above `DUST_THRESHOLD`; otherwise throws.
|
|
54
54
|
* @throws {BlacklistViolationError} when net inbound flow to a restricted
|
|
55
|
-
* `bundles` or `
|
|
55
|
+
* `bundles` or `midnightBundlesV2` contract exceeds `DUST_THRESHOLD` for any
|
|
56
56
|
* token.
|
|
57
57
|
*/
|
|
58
58
|
export function assertNoBundlesRetention(params) {
|
|
59
59
|
const { chainId, transfers, assetChanges, logger } = params;
|
|
60
|
-
// Standalone bundles contracts and `
|
|
60
|
+
// Standalone bundles contracts and `MidnightBundlesV2` are transient
|
|
61
61
|
// intermediaries that route user value and must never retain it.
|
|
62
62
|
let addresses;
|
|
63
63
|
try {
|
|
@@ -79,8 +79,8 @@ export function assertNoBundlesRetention(params) {
|
|
|
79
79
|
for (const addr of Object.values(addresses.bundles).filter(isDefined))
|
|
80
80
|
restrictedAddresses.add(getAddress(addr));
|
|
81
81
|
}
|
|
82
|
-
if (addresses.
|
|
83
|
-
restrictedAddresses.add(getAddress(addresses.
|
|
82
|
+
if (addresses.midnightBundlesV2) {
|
|
83
|
+
restrictedAddresses.add(getAddress(addresses.midnightBundlesV2));
|
|
84
84
|
}
|
|
85
85
|
if (restrictedAddresses.size === 0) {
|
|
86
86
|
// blue-sdk knows the chain but cataloged no restricted intermediary for it.
|
|
@@ -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
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Address, type BlockTag, type Hex } from "viem";
|
|
2
2
|
import type { SimulationAuthorization } from "../../authorizations.js";
|
|
3
3
|
import type { SimulationLimits } from "../../limits.js";
|
|
4
|
-
import { type SimulateParams, type SimulationMode } from "../../params.js";
|
|
4
|
+
import { type SimulateParams, type SimulationMode, type StateBlock } from "../../params.js";
|
|
5
5
|
/** A normalized user transaction: checksummed addresses, `value` defaulted to `0n`.
|
|
6
6
|
* @internal
|
|
7
7
|
*/
|
|
@@ -21,6 +21,7 @@ export interface ParsedRequest {
|
|
|
21
21
|
/** Always empty in final mode. */
|
|
22
22
|
readonly authorizations: readonly SimulationAuthorization[];
|
|
23
23
|
readonly blockNumber?: bigint | Exclude<BlockTag, "pending">;
|
|
24
|
+
readonly block?: StateBlock;
|
|
24
25
|
readonly limits?: SimulationLimits;
|
|
25
26
|
}
|
|
26
27
|
/**
|
|
@@ -49,6 +49,7 @@ const PERMIT2_MESSAGE_KEYS = PERMIT2_TRANSFER_FIELDS.map((field) => field.name);
|
|
|
49
49
|
const PERMIT2_PERMITTED_KEYS = ["token", "amount"];
|
|
50
50
|
const BLUE_AUTHORIZATION_MESSAGE_KEYS = BLUE_AUTHORIZATION_FIELDS.map((field) => field.name);
|
|
51
51
|
const TRANSACTION_KEYS = ["from", "to", "data", "value"];
|
|
52
|
+
const BLOCK_KEYS = ["number", "hash", "timestamp"];
|
|
52
53
|
const LIMITS_KEYS = ["operations"];
|
|
53
54
|
const AUTHORIZATION_KEYS = {
|
|
54
55
|
erc20Approval: ["type", "token", "owner", "spender", "amount"],
|
|
@@ -567,6 +568,7 @@ export function parseRequest(input) {
|
|
|
567
568
|
"chainId",
|
|
568
569
|
"transactions",
|
|
569
570
|
"blockNumber",
|
|
571
|
+
"block",
|
|
570
572
|
"mode",
|
|
571
573
|
"authorizations",
|
|
572
574
|
"limits",
|
|
@@ -649,6 +651,24 @@ export function parseRequest(input) {
|
|
|
649
651
|
}
|
|
650
652
|
}
|
|
651
653
|
}
|
|
654
|
+
// block
|
|
655
|
+
const rawBlock = input.block;
|
|
656
|
+
let block;
|
|
657
|
+
if (rawBlock !== undefined) {
|
|
658
|
+
if (!isRecord(rawBlock)) {
|
|
659
|
+
fieldErrors.push("block: must be an object");
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
check.keys(rawBlock, { allow: BLOCK_KEYS, path: "block" });
|
|
663
|
+
const number = check.uint256(readField(rawBlock, "number"), "block.number");
|
|
664
|
+
const hash = check.bytes32(readField(rawBlock, "hash"), "block.hash");
|
|
665
|
+
const timestamp = check.uint256(readField(rawBlock, "timestamp"), "block.timestamp");
|
|
666
|
+
if (number !== undefined && hash !== undefined && timestamp !== undefined)
|
|
667
|
+
block = { number, hash: hash.toLowerCase(), timestamp };
|
|
668
|
+
}
|
|
669
|
+
if (rawBlockNumber !== undefined)
|
|
670
|
+
fieldErrors.push("block: cannot be combined with blockNumber");
|
|
671
|
+
}
|
|
652
672
|
// authorizations
|
|
653
673
|
const rawAuthorizations = input.authorizations;
|
|
654
674
|
const authorizations = [];
|
|
@@ -751,6 +771,7 @@ export function parseRequest(input) {
|
|
|
751
771
|
transactions,
|
|
752
772
|
authorizations: mode === "preview" ? authorizations : [],
|
|
753
773
|
...(blockNumber !== undefined ? { blockNumber } : {}),
|
|
774
|
+
...(block !== undefined ? { block } : {}),
|
|
754
775
|
...(normalizedLimits !== undefined ? { limits: normalizedLimits } : {}),
|
|
755
776
|
});
|
|
756
777
|
}
|
|
@@ -4,7 +4,7 @@ import { zeroAddress } from "viem";
|
|
|
4
4
|
import { InvalidSimulationResponseError, UnsupportedChainError, } from "../errors.js";
|
|
5
5
|
import { groupAssetChanges } from "./asset-changes.js";
|
|
6
6
|
import { createSimulationClient } from "./backends/client.js";
|
|
7
|
-
import {
|
|
7
|
+
import { executePlan } from "./backends/index.js";
|
|
8
8
|
import { resolveAssets } from "./backends/resolve-assets.js";
|
|
9
9
|
import { resolvePinnedBlock } from "./backends/resolve-pinned-block.js";
|
|
10
10
|
import { parseTransfers } from "./parsing/index.js";
|
|
@@ -42,21 +42,16 @@ export async function runSimulation(params) {
|
|
|
42
42
|
if (requiresMorpho && addresses?.blue == null)
|
|
43
43
|
throw new UnsupportedChainError(request.chainId);
|
|
44
44
|
const morpho = addresses?.blue ?? zeroAddress;
|
|
45
|
-
await assertEndpointChain({
|
|
46
|
-
client,
|
|
47
|
-
chainId: request.chainId,
|
|
48
|
-
mode: request.mode,
|
|
49
|
-
blockNumber: typeof request.blockNumber === "bigint" ? request.blockNumber : undefined,
|
|
50
|
-
});
|
|
51
45
|
// Monad nodes reject `validation: false` and charge no gas in the simulated
|
|
52
46
|
// block either way. Its `latest` block is not final: simulating at its number
|
|
53
47
|
// runs on another parent, so default to `finalized`.
|
|
54
48
|
const monad = request.chainId === ChainId.MonadMainnet;
|
|
55
|
-
const pinnedBlock =
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
49
|
+
const pinnedBlock = request.block ??
|
|
50
|
+
(await resolvePinnedBlock({
|
|
51
|
+
client,
|
|
52
|
+
blockNumber: request.blockNumber ?? (monad ? "finalized" : undefined),
|
|
53
|
+
signal,
|
|
54
|
+
}));
|
|
60
55
|
// With no calldata decoding, the caller's declared `limits.operations`
|
|
61
56
|
// entries are the operations: subjects for state reads and per-operation
|
|
62
57
|
// checks come straight from the limit list.
|
|
@@ -82,12 +77,14 @@ export async function runSimulation(params) {
|
|
|
82
77
|
preparations,
|
|
83
78
|
reads: observations.reads,
|
|
84
79
|
});
|
|
85
|
-
// Execute against the block resolved once above
|
|
80
|
+
// Execute against the block resolved once above.
|
|
86
81
|
const execution = await executePlan({
|
|
87
82
|
client,
|
|
88
83
|
plan,
|
|
89
84
|
stateBlock: pinnedBlock,
|
|
90
85
|
validation: monad,
|
|
86
|
+
blockGasLimit: chain.blockOverrides?.gasLimit,
|
|
87
|
+
parentHashCheck: chain.parentHashCheck ?? request.chainId !== ChainId.StableMainnet,
|
|
91
88
|
});
|
|
92
89
|
const userCalls = execution.calls
|
|
93
90
|
.filter((call) => call.planned.type === "transaction")
|