@morpho-org/evm-simulation 5.1.0 → 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 +21 -34
- package/lib/cjs/simulate/backends/eth-simulate-v1.js +27 -73
- 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.js +1 -1
- 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/request/parse-request.d.ts +2 -1
- package/lib/cjs/simulate/request/parse-request.js +21 -0
- package/lib/cjs/simulate/run-simulation.js +7 -12
- package/lib/cjs/simulate/simulate.d.ts +25 -17
- package/lib/cjs/simulate/simulate.js +25 -17
- package/lib/cjs/types.d.ts +1 -1
- 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 +21 -34
- package/lib/esm/simulate/backends/eth-simulate-v1.js +29 -74
- 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.js +1 -1
- 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/request/parse-request.d.ts +2 -1
- package/lib/esm/simulate/request/parse-request.js +21 -0
- package/lib/esm/simulate/run-simulation.js +8 -13
- package/lib/esm/simulate/simulate.d.ts +25 -17
- package/lib/esm/simulate/simulate.js +25 -17
- package/lib/esm/types.d.ts +1 -1
- package/package.json +4 -4
|
@@ -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,38 +61,15 @@ 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
75
|
* gas is separated from economic effects. When `blockGasLimit` is set,
|
|
@@ -92,32 +78,31 @@ export async function assertEndpointChain(params) {
|
|
|
92
78
|
* **No balance override is
|
|
93
79
|
* applied** — `value` transfers are funded by the sender's real native
|
|
94
80
|
* balance.
|
|
95
|
-
* 2. **Response validation** — the
|
|
96
|
-
* re-fetch so revert/mismatch evidence already in hand surfaces instead
|
|
97
|
-
* of being downgraded to a bypassable {@link ExternalServiceError} by a
|
|
98
|
-
* failing re-fetch. The simulated block must be exactly
|
|
81
|
+
* 2. **Response validation** — the simulated block must be exactly
|
|
99
82
|
* `stateBlockNumber` or `stateBlockNumber + 1`: geth-style nodes report
|
|
100
83
|
* the former's successor while Anvil reports the pinned block itself.
|
|
101
84
|
* The result records whatever the node returns; consumers must read
|
|
102
85
|
* {@link ExecutionBlock.blockNumber} and never assume +1.
|
|
103
|
-
* 3. **Reorg check** — the pinned state block is re-fetched last to detect a reorg
|
|
104
|
-
* that swapped its hash mid-flight (`InvalidSimulationResponseError`).
|
|
105
86
|
*
|
|
106
87
|
* The endpoint must support `eth_simulateV1` with per-call `from`; there is
|
|
107
88
|
* no fallback backend.
|
|
108
89
|
*
|
|
109
|
-
* @param params - Shared simulation client, execution plan
|
|
110
|
-
* state block, the `validation` flag to
|
|
111
|
-
* block gas limit, and whether to check the
|
|
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`.
|
|
112
94
|
* @returns Deep-frozen {@link SimulationExecution} — per-transaction call
|
|
113
95
|
* results and the resolved {@link ExecutionBlock}.
|
|
114
96
|
* @throws {ExternalServiceError} For transport failures, timeouts,
|
|
115
97
|
* or malformed JSON-RPC envelopes.
|
|
116
98
|
* @throws {InvalidSimulationResponseError} For a response that cannot be
|
|
117
|
-
* trusted (bad shape, call-count mismatch, block
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
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).
|
|
121
106
|
* @throws {MissingVerificationEvidenceError} When a planned state read fails.
|
|
122
107
|
* @throws {SimulationRevertedError} When a preparation or user transaction
|
|
123
108
|
* reverts or the node reports a bundle-level revert (code 3 / insufficient
|
|
@@ -136,6 +121,7 @@ export async function executePlan(params) {
|
|
|
136
121
|
blockOverrides: { gasLimit: numberToHex(blockGasLimit) },
|
|
137
122
|
}),
|
|
138
123
|
calls: plan.calls.map((call) => ({
|
|
124
|
+
chainId: numberToHex(plan.request.chainId),
|
|
139
125
|
from: call.transaction.from,
|
|
140
126
|
to: call.transaction.to,
|
|
141
127
|
data: call.transaction.data,
|
|
@@ -149,12 +135,7 @@ export async function executePlan(params) {
|
|
|
149
135
|
numberToHex(stateBlock.number),
|
|
150
136
|
],
|
|
151
137
|
}));
|
|
152
|
-
|
|
153
|
-
// reorg re-fetch so evidence already in hand reaches the caller as
|
|
154
|
-
// InvalidSimulationResponseError / SimulationRevertedError instead of
|
|
155
|
-
// being downgraded to a bypassable ExternalServiceError by a failing
|
|
156
|
-
// re-fetch.
|
|
157
|
-
const execution = parseSimulationResponse({
|
|
138
|
+
return parseSimulationResponse({
|
|
158
139
|
plan,
|
|
159
140
|
blocks: response,
|
|
160
141
|
stateBlockNumber: stateBlock.number,
|
|
@@ -162,30 +143,4 @@ export async function executePlan(params) {
|
|
|
162
143
|
stateBlockTimestamp: stateBlock.timestamp,
|
|
163
144
|
parentHashCheck,
|
|
164
145
|
});
|
|
165
|
-
// Reorg window: the pinned state block must still carry the same hash
|
|
166
|
-
// after simulation, or the result may describe a different chain tip.
|
|
167
|
-
const stateBlockAfter = await rpc("eth_getBlock", () => client.getBlock({ blockNumber: stateBlock.number })).catch((error) => {
|
|
168
|
-
if (error instanceof ExternalServiceError &&
|
|
169
|
-
error.cause instanceof BlockNotFoundError)
|
|
170
|
-
return error;
|
|
171
|
-
throw error;
|
|
172
|
-
});
|
|
173
|
-
if (stateBlockAfter instanceof ExternalServiceError ||
|
|
174
|
-
stateBlockAfter.hash !== stateBlock.hash) {
|
|
175
|
-
const after = stateBlockAfter instanceof ExternalServiceError
|
|
176
|
-
? "unavailable"
|
|
177
|
-
: stateBlockAfter.hash;
|
|
178
|
-
throw new InvalidSimulationResponseError(`State block ${stateBlock.number} hash changed during simulation (reorg): ${stateBlock.hash} became ${after}. Re-submit the simulation.`, {
|
|
179
|
-
context: {
|
|
180
|
-
stage: "transport",
|
|
181
|
-
chainId: plan.request.chainId,
|
|
182
|
-
mode: plan.request.mode,
|
|
183
|
-
blockNumber: stateBlock.number,
|
|
184
|
-
},
|
|
185
|
-
...(stateBlockAfter instanceof ExternalServiceError && {
|
|
186
|
-
cause: stateBlockAfter.cause,
|
|
187
|
-
}),
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
return execution;
|
|
191
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";
|
|
@@ -55,7 +55,7 @@ export function parseSimulationResponse(params) {
|
|
|
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
57
|
// the pinned state block hash. With `parentHashCheck` off (Stable by
|
|
58
|
-
// default) the block number
|
|
58
|
+
// default) the block number and timestamp checks still apply.
|
|
59
59
|
if (params.parentHashCheck &&
|
|
60
60
|
blockNumber === params.stateBlockNumber + 1n &&
|
|
61
61
|
block.parentHash !== params.stateBlockHash) {
|
|
@@ -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.
|
|
@@ -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,7 +77,7 @@ 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,
|
|
@@ -4,11 +4,11 @@ import type { SimulationConfig } from "../types.js";
|
|
|
4
4
|
/**
|
|
5
5
|
* Simulate a bundle of EVM transactions.
|
|
6
6
|
*
|
|
7
|
-
* Parses and normalizes the request → resolves
|
|
8
|
-
*
|
|
9
|
-
* and the execution → executes once through `eth_simulateV1` under
|
|
10
|
-
* timeout budget (simulation with in-block state reads → response
|
|
11
|
-
*
|
|
7
|
+
* Parses and normalizes the request → resolves a single pinned block (skipped
|
|
8
|
+
* when `params.block` is supplied) → resolves quoted-asset metadata → plans
|
|
9
|
+
* state reads and the execution → executes once through `eth_simulateV1` under
|
|
10
|
+
* the full timeout budget (simulation with in-block state reads → response
|
|
11
|
+
* parsing) → derives ERC20/WETH9 transfers and net asset changes from the
|
|
12
12
|
* user calls only → decodes quoted balances/positions → runs slippage checks
|
|
13
13
|
* → asserts no funds are retained by standalone `bundles` periphery contracts
|
|
14
14
|
* → returns the result. The caller reads whichever fields they need:
|
|
@@ -41,7 +41,9 @@ import type { SimulationConfig } from "../types.js";
|
|
|
41
41
|
* `blockOverrides.gasLimit` and `parentHashCheck`, optional logger, and the
|
|
42
42
|
* overall timeout budget.
|
|
43
43
|
* @param params - Per-call simulation input.
|
|
44
|
-
* @param params.chainId - Chain id the bundle targets
|
|
44
|
+
* @param params.chainId - Chain id the bundle targets. Sent on every call inside
|
|
45
|
+
* `eth_simulateV1`, so nodes that check it reject a wrong-chain endpoint;
|
|
46
|
+
* there is no separate `eth_chainId` lookup.
|
|
45
47
|
* @param params.transactions - The bundle's transactions, in execution order.
|
|
46
48
|
* All must share the same `from`.
|
|
47
49
|
* @param params.mode - `"final"` (default) or `"preview"`.
|
|
@@ -51,9 +53,14 @@ import type { SimulationConfig } from "../types.js";
|
|
|
51
53
|
* than `"pending"`.
|
|
52
54
|
* Defaults to `latest` (`finalized` on Monad, whose `latest` block is not
|
|
53
55
|
* final), resolved exactly once.
|
|
56
|
+
* @param params.block - Optional caller-supplied state block (`number`,
|
|
57
|
+
* `hash`, `timestamp`). Skips the block lookup, so a simulation without
|
|
58
|
+
* asset metadata reads makes a single `eth_simulateV1` request. Cannot be
|
|
59
|
+
* combined with `blockNumber`.
|
|
54
60
|
* @throws {SimulationValidationError} for invalid input (mixed senders, bad
|
|
55
61
|
* addresses, empty transactions, malformed authorizations, final-mode
|
|
56
|
-
* authorizations, malformed limits, unknown fields, a `"pending"` block tag,
|
|
62
|
+
* authorizations, malformed limits, unknown fields, a `"pending"` block tag,
|
|
63
|
+
* a malformed `block` or one combined with `blockNumber`, a
|
|
57
64
|
* `blockOverrides.gasLimit` that is not a positive bigint, a `parentHashCheck`
|
|
58
65
|
* that is not a boolean, or share quotes for
|
|
59
66
|
* `blueSupplyCollateral` / `blueWithdrawCollateral`).
|
|
@@ -69,20 +76,21 @@ import type { SimulationConfig } from "../types.js";
|
|
|
69
76
|
* @throws {InvalidSimulationResponseError} when the node response cannot be
|
|
70
77
|
* trusted (bad shape, call-count mismatch, block that is neither the pinned
|
|
71
78
|
* state block nor its immediate successor, a successor with a mismatched
|
|
72
|
-
* `parentHash` (unless `parentHashCheck` is off; off by default on Stable,
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
79
|
+
* `parentHash` (unless `parentHashCheck` is off; off by default on Stable,
|
|
80
|
+
* chain 988), a block timestamp earlier than the pinned block's, a
|
|
81
|
+
* malformed per-call result, or a quoted balance/position read whose
|
|
82
|
+
* non-empty return data cannot be decoded).
|
|
83
|
+
* @throws {InvalidChainIdError} when the node rejects the request `chainId`
|
|
84
|
+
* because the endpoint serves another chain (a subclass of
|
|
85
|
+
* `InvalidSimulationResponseError` sharing its `name`/`code`;
|
|
86
|
+
* `instanceof InvalidChainIdError` tells them apart). Never bypassable.
|
|
78
87
|
* @throws {BlacklistViolationError} when the simulation leaves value retained
|
|
79
88
|
* beyond the dust threshold by a `bundles` periphery contract
|
|
80
|
-
* (VaultExitBundlesV1, VaultBundlesV1, BlueBundlesV1,
|
|
89
|
+
* (VaultExitBundlesV1, VaultBundlesV1, BlueBundlesV1, MidnightBundlesV2).
|
|
81
90
|
* Never bypassable.
|
|
82
91
|
* @throws {ExternalServiceError} when the RPC is unavailable within the
|
|
83
|
-
* timeout budget or returns a malformed JSON-RPC envelope.
|
|
84
|
-
*
|
|
85
|
-
* `InvalidSimulationResponseError`.
|
|
92
|
+
* timeout budget or returns a malformed JSON-RPC envelope. A looked-up state
|
|
93
|
+
* block without number/hash is reported as `InvalidSimulationResponseError`.
|
|
86
94
|
* @returns A frozen {@link VerifiedSimulationResult} carrying the normalized
|
|
87
95
|
* `simulationTxs`, per-tx `calls` (aligned 1:1), parsed `transfers` (each
|
|
88
96
|
* stamped with `txIdx`), per-account net `assetChanges`, and per-operation
|
|
@@ -3,11 +3,11 @@ import { runSimulation } from "./run-simulation.js";
|
|
|
3
3
|
/**
|
|
4
4
|
* Simulate a bundle of EVM transactions.
|
|
5
5
|
*
|
|
6
|
-
* Parses and normalizes the request → resolves
|
|
7
|
-
*
|
|
8
|
-
* and the execution → executes once through `eth_simulateV1` under
|
|
9
|
-
* timeout budget (simulation with in-block state reads → response
|
|
10
|
-
*
|
|
6
|
+
* Parses and normalizes the request → resolves a single pinned block (skipped
|
|
7
|
+
* when `params.block` is supplied) → resolves quoted-asset metadata → plans
|
|
8
|
+
* state reads and the execution → executes once through `eth_simulateV1` under
|
|
9
|
+
* the full timeout budget (simulation with in-block state reads → response
|
|
10
|
+
* parsing) → derives ERC20/WETH9 transfers and net asset changes from the
|
|
11
11
|
* user calls only → decodes quoted balances/positions → runs slippage checks
|
|
12
12
|
* → asserts no funds are retained by standalone `bundles` periphery contracts
|
|
13
13
|
* → returns the result. The caller reads whichever fields they need:
|
|
@@ -40,7 +40,9 @@ import { runSimulation } from "./run-simulation.js";
|
|
|
40
40
|
* `blockOverrides.gasLimit` and `parentHashCheck`, optional logger, and the
|
|
41
41
|
* overall timeout budget.
|
|
42
42
|
* @param params - Per-call simulation input.
|
|
43
|
-
* @param params.chainId - Chain id the bundle targets
|
|
43
|
+
* @param params.chainId - Chain id the bundle targets. Sent on every call inside
|
|
44
|
+
* `eth_simulateV1`, so nodes that check it reject a wrong-chain endpoint;
|
|
45
|
+
* there is no separate `eth_chainId` lookup.
|
|
44
46
|
* @param params.transactions - The bundle's transactions, in execution order.
|
|
45
47
|
* All must share the same `from`.
|
|
46
48
|
* @param params.mode - `"final"` (default) or `"preview"`.
|
|
@@ -50,9 +52,14 @@ import { runSimulation } from "./run-simulation.js";
|
|
|
50
52
|
* than `"pending"`.
|
|
51
53
|
* Defaults to `latest` (`finalized` on Monad, whose `latest` block is not
|
|
52
54
|
* final), resolved exactly once.
|
|
55
|
+
* @param params.block - Optional caller-supplied state block (`number`,
|
|
56
|
+
* `hash`, `timestamp`). Skips the block lookup, so a simulation without
|
|
57
|
+
* asset metadata reads makes a single `eth_simulateV1` request. Cannot be
|
|
58
|
+
* combined with `blockNumber`.
|
|
53
59
|
* @throws {SimulationValidationError} for invalid input (mixed senders, bad
|
|
54
60
|
* addresses, empty transactions, malformed authorizations, final-mode
|
|
55
|
-
* authorizations, malformed limits, unknown fields, a `"pending"` block tag,
|
|
61
|
+
* authorizations, malformed limits, unknown fields, a `"pending"` block tag,
|
|
62
|
+
* a malformed `block` or one combined with `blockNumber`, a
|
|
56
63
|
* `blockOverrides.gasLimit` that is not a positive bigint, a `parentHashCheck`
|
|
57
64
|
* that is not a boolean, or share quotes for
|
|
58
65
|
* `blueSupplyCollateral` / `blueWithdrawCollateral`).
|
|
@@ -68,20 +75,21 @@ import { runSimulation } from "./run-simulation.js";
|
|
|
68
75
|
* @throws {InvalidSimulationResponseError} when the node response cannot be
|
|
69
76
|
* trusted (bad shape, call-count mismatch, block that is neither the pinned
|
|
70
77
|
* state block nor its immediate successor, a successor with a mismatched
|
|
71
|
-
* `parentHash` (unless `parentHashCheck` is off; off by default on Stable,
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
78
|
+
* `parentHash` (unless `parentHashCheck` is off; off by default on Stable,
|
|
79
|
+
* chain 988), a block timestamp earlier than the pinned block's, a
|
|
80
|
+
* malformed per-call result, or a quoted balance/position read whose
|
|
81
|
+
* non-empty return data cannot be decoded).
|
|
82
|
+
* @throws {InvalidChainIdError} when the node rejects the request `chainId`
|
|
83
|
+
* because the endpoint serves another chain (a subclass of
|
|
84
|
+
* `InvalidSimulationResponseError` sharing its `name`/`code`;
|
|
85
|
+
* `instanceof InvalidChainIdError` tells them apart). Never bypassable.
|
|
77
86
|
* @throws {BlacklistViolationError} when the simulation leaves value retained
|
|
78
87
|
* beyond the dust threshold by a `bundles` periphery contract
|
|
79
|
-
* (VaultExitBundlesV1, VaultBundlesV1, BlueBundlesV1,
|
|
88
|
+
* (VaultExitBundlesV1, VaultBundlesV1, BlueBundlesV1, MidnightBundlesV2).
|
|
80
89
|
* Never bypassable.
|
|
81
90
|
* @throws {ExternalServiceError} when the RPC is unavailable within the
|
|
82
|
-
* timeout budget or returns a malformed JSON-RPC envelope.
|
|
83
|
-
*
|
|
84
|
-
* `InvalidSimulationResponseError`.
|
|
91
|
+
* timeout budget or returns a malformed JSON-RPC envelope. A looked-up state
|
|
92
|
+
* block without number/hash is reported as `InvalidSimulationResponseError`.
|
|
85
93
|
* @returns A frozen {@link VerifiedSimulationResult} carrying the normalized
|
|
86
94
|
* `simulationTxs`, per-tx `calls` (aligned 1:1), parsed `transfers` (each
|
|
87
95
|
* stamped with `txIdx`), per-account net `assetChanges`, and per-operation
|
package/lib/esm/types.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export interface ChainSimulationConfig {
|
|
|
13
13
|
};
|
|
14
14
|
/**
|
|
15
15
|
* Whether a simulated successor block's `parentHash` must equal the pinned
|
|
16
|
-
* state block hash. Block number
|
|
16
|
+
* state block hash. Block number and timestamp checks always run.
|
|
17
17
|
* Omitted: on everywhere except Stable (988), whose nodes report a
|
|
18
18
|
* `parentHash` that never matches the pinned block.
|
|
19
19
|
*/
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morpho-org/evm-simulation",
|
|
3
3
|
"description": "EVM simulation engine for Morpho transactions.",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "6.0.0",
|
|
5
5
|
"author": "Morpho Association <contact@morpho.org>",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
"lib"
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@morpho-org/blue-sdk": "^
|
|
25
|
-
"@morpho-org/morpho-sdk": "^
|
|
26
|
-
"@morpho-org/morpho-ts": "^
|
|
24
|
+
"@morpho-org/blue-sdk": "^8.0.0",
|
|
25
|
+
"@morpho-org/morpho-sdk": "^7.0.0",
|
|
26
|
+
"@morpho-org/morpho-ts": "^4.0.0"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
29
|
"viem": "^2.0.0"
|