@morpho-org/evm-simulation 4.2.0 → 5.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 +109 -17
- package/lib/cjs/authorizations.d.ts +95 -0
- package/lib/cjs/authorizations.js +2 -0
- package/lib/cjs/errors.d.ts +207 -13
- package/lib/cjs/errors.js +197 -13
- package/lib/cjs/index.d.ts +10 -2
- package/lib/cjs/index.js +19 -3
- package/lib/cjs/limits.d.ts +112 -0
- package/lib/cjs/limits.js +50 -0
- package/lib/cjs/params.d.ts +25 -0
- package/lib/cjs/params.js +5 -0
- package/lib/cjs/requirements/index.d.ts +1 -0
- package/lib/cjs/requirements/index.js +5 -0
- package/lib/cjs/requirements/requirements.d.ts +64 -0
- package/lib/cjs/requirements/requirements.js +149 -0
- package/lib/cjs/result.d.ts +49 -0
- package/lib/cjs/result.js +2 -0
- package/lib/cjs/simulate/asset-changes.d.ts +7 -5
- package/lib/cjs/simulate/asset-changes.js +6 -5
- package/lib/cjs/simulate/backends/client.d.ts +16 -0
- package/lib/cjs/simulate/backends/client.js +27 -0
- package/lib/cjs/simulate/backends/eth-simulate-v1.d.ts +66 -20
- package/lib/cjs/simulate/backends/eth-simulate-v1.js +172 -98
- package/lib/cjs/simulate/backends/index.d.ts +1 -2
- package/lib/cjs/simulate/backends/index.js +3 -4
- package/lib/cjs/simulate/backends/parse-response.d.ts +69 -0
- package/lib/cjs/simulate/backends/parse-response.js +168 -0
- package/lib/cjs/simulate/backends/resolve-assets.d.ts +26 -0
- package/lib/cjs/simulate/backends/resolve-assets.js +111 -0
- package/lib/cjs/simulate/backends/resolve-pinned-block.d.ts +33 -0
- package/lib/cjs/simulate/backends/resolve-pinned-block.js +46 -0
- package/lib/cjs/simulate/context.d.ts +14 -0
- package/lib/cjs/simulate/context.js +2 -0
- package/lib/cjs/simulate/measurement-plan.d.ts +29 -0
- package/lib/cjs/simulate/measurement-plan.js +115 -0
- package/lib/cjs/simulate/parsing/transfers.d.ts +4 -3
- package/lib/cjs/simulate/parsing/transfers.js +4 -6
- package/lib/cjs/simulate/pipeline/bundles-retention.d.ts +7 -10
- package/lib/cjs/simulate/pipeline/bundles-retention.js +11 -14
- package/lib/cjs/simulate/pipeline/resolve-chain.d.ts +5 -11
- package/lib/cjs/simulate/pipeline/resolve-chain.js +5 -10
- package/lib/cjs/simulate/plan/plan-execution.d.ts +58 -0
- package/lib/cjs/simulate/plan/plan-execution.js +75 -0
- package/lib/cjs/simulate/prepare.d.ts +30 -0
- package/lib/cjs/simulate/prepare.js +80 -0
- package/lib/cjs/simulate/request/index.d.ts +1 -0
- package/lib/cjs/simulate/request/index.js +5 -0
- package/lib/cjs/simulate/request/parse-request.d.ts +44 -0
- package/lib/cjs/simulate/request/parse-request.js +759 -0
- package/lib/cjs/simulate/result.d.ts +19 -0
- package/lib/cjs/simulate/result.js +29 -0
- package/lib/cjs/simulate/run-simulation.d.ts +18 -0
- package/lib/cjs/simulate/run-simulation.js +151 -0
- package/lib/cjs/simulate/simulate.d.ts +100 -50
- package/lib/cjs/simulate/simulate.js +99 -81
- package/lib/cjs/simulate/slippage.d.ts +18 -0
- package/lib/cjs/simulate/slippage.js +41 -0
- package/lib/cjs/simulate/state/contract.d.ts +19 -0
- package/lib/cjs/simulate/state/contract.js +2 -0
- package/lib/cjs/simulate/state/read-state.d.ts +60 -0
- package/lib/cjs/simulate/state/read-state.js +148 -0
- package/lib/cjs/simulate/verify-slippage.d.ts +24 -0
- package/lib/cjs/simulate/verify-slippage.js +85 -0
- package/lib/cjs/types.d.ts +35 -95
- package/lib/esm/authorizations.d.ts +95 -0
- package/lib/esm/authorizations.js +1 -0
- package/lib/esm/errors.d.ts +207 -13
- package/lib/esm/errors.js +189 -12
- package/lib/esm/index.d.ts +10 -2
- package/lib/esm/index.js +4 -1
- package/lib/esm/limits.d.ts +112 -0
- package/lib/esm/limits.js +47 -0
- package/lib/esm/params.d.ts +25 -0
- package/lib/esm/params.js +2 -0
- package/lib/esm/requirements/index.d.ts +1 -0
- package/lib/esm/requirements/index.js +1 -0
- package/lib/esm/requirements/requirements.d.ts +64 -0
- package/lib/esm/requirements/requirements.js +146 -0
- package/lib/esm/result.d.ts +49 -0
- package/lib/esm/result.js +1 -0
- package/lib/esm/simulate/asset-changes.d.ts +7 -5
- package/lib/esm/simulate/asset-changes.js +6 -5
- package/lib/esm/simulate/backends/client.d.ts +16 -0
- package/lib/esm/simulate/backends/client.js +24 -0
- package/lib/esm/simulate/backends/eth-simulate-v1.d.ts +66 -20
- package/lib/esm/simulate/backends/eth-simulate-v1.js +170 -99
- package/lib/esm/simulate/backends/index.d.ts +1 -2
- package/lib/esm/simulate/backends/index.js +1 -2
- package/lib/esm/simulate/backends/parse-response.d.ts +69 -0
- package/lib/esm/simulate/backends/parse-response.js +165 -0
- package/lib/esm/simulate/backends/resolve-assets.d.ts +26 -0
- package/lib/esm/simulate/backends/resolve-assets.js +108 -0
- package/lib/esm/simulate/backends/resolve-pinned-block.d.ts +33 -0
- package/lib/esm/simulate/backends/resolve-pinned-block.js +43 -0
- package/lib/esm/simulate/context.d.ts +14 -0
- package/lib/esm/simulate/context.js +1 -0
- package/lib/esm/simulate/measurement-plan.d.ts +29 -0
- package/lib/esm/simulate/measurement-plan.js +112 -0
- package/lib/esm/simulate/parsing/transfers.d.ts +4 -3
- package/lib/esm/simulate/parsing/transfers.js +4 -6
- package/lib/esm/simulate/pipeline/bundles-retention.d.ts +7 -10
- package/lib/esm/simulate/pipeline/bundles-retention.js +11 -14
- package/lib/esm/simulate/pipeline/resolve-chain.d.ts +5 -11
- package/lib/esm/simulate/pipeline/resolve-chain.js +5 -10
- package/lib/esm/simulate/plan/plan-execution.d.ts +58 -0
- package/lib/esm/simulate/plan/plan-execution.js +72 -0
- package/lib/esm/simulate/prepare.d.ts +30 -0
- package/lib/esm/simulate/prepare.js +77 -0
- package/lib/esm/simulate/request/index.d.ts +1 -0
- package/lib/esm/simulate/request/index.js +1 -0
- package/lib/esm/simulate/request/parse-request.d.ts +44 -0
- package/lib/esm/simulate/request/parse-request.js +756 -0
- package/lib/esm/simulate/result.d.ts +19 -0
- package/lib/esm/simulate/result.js +26 -0
- package/lib/esm/simulate/run-simulation.d.ts +18 -0
- package/lib/esm/simulate/run-simulation.js +148 -0
- package/lib/esm/simulate/simulate.d.ts +100 -50
- package/lib/esm/simulate/simulate.js +99 -81
- package/lib/esm/simulate/slippage.d.ts +18 -0
- package/lib/esm/simulate/slippage.js +38 -0
- package/lib/esm/simulate/state/contract.d.ts +19 -0
- package/lib/esm/simulate/state/contract.js +1 -0
- package/lib/esm/simulate/state/read-state.d.ts +60 -0
- package/lib/esm/simulate/state/read-state.js +144 -0
- package/lib/esm/simulate/verify-slippage.d.ts +24 -0
- package/lib/esm/simulate/verify-slippage.js +82 -0
- package/lib/esm/types.d.ts +35 -95
- package/package.json +7 -6
- package/lib/cjs/simulate/authorizations/index.d.ts +0 -2
- package/lib/cjs/simulate/authorizations/index.js +0 -7
- package/lib/cjs/simulate/authorizations/resolve.d.ts +0 -9
- package/lib/cjs/simulate/authorizations/resolve.js +0 -29
- package/lib/cjs/simulate/authorizations/validate.d.ts +0 -6
- package/lib/cjs/simulate/authorizations/validate.js +0 -23
- package/lib/cjs/simulate/backends/tenderly-rpc.d.ts +0 -23
- package/lib/cjs/simulate/backends/tenderly-rpc.js +0 -234
- package/lib/cjs/simulate/pipeline/build-simulation-txs.d.ts +0 -17
- package/lib/cjs/simulate/pipeline/build-simulation-txs.js +0 -50
- package/lib/cjs/simulate/pipeline/execute-simulation.d.ts +0 -22
- package/lib/cjs/simulate/pipeline/execute-simulation.js +0 -82
- package/lib/cjs/simulate/pipeline/index.d.ts +0 -4
- package/lib/cjs/simulate/pipeline/index.js +0 -11
- package/lib/cjs/simulate/pipeline/validate-input.d.ts +0 -10
- package/lib/cjs/simulate/pipeline/validate-input.js +0 -55
- package/lib/esm/simulate/authorizations/index.d.ts +0 -2
- package/lib/esm/simulate/authorizations/index.js +0 -2
- package/lib/esm/simulate/authorizations/resolve.d.ts +0 -9
- package/lib/esm/simulate/authorizations/resolve.js +0 -26
- package/lib/esm/simulate/authorizations/validate.d.ts +0 -6
- package/lib/esm/simulate/authorizations/validate.js +0 -20
- package/lib/esm/simulate/backends/tenderly-rpc.d.ts +0 -23
- package/lib/esm/simulate/backends/tenderly-rpc.js +0 -231
- package/lib/esm/simulate/pipeline/build-simulation-txs.d.ts +0 -17
- package/lib/esm/simulate/pipeline/build-simulation-txs.js +0 -47
- package/lib/esm/simulate/pipeline/execute-simulation.d.ts +0 -22
- package/lib/esm/simulate/pipeline/execute-simulation.js +0 -79
- package/lib/esm/simulate/pipeline/index.d.ts +0 -4
- package/lib/esm/simulate/pipeline/index.js +0 -4
- package/lib/esm/simulate/pipeline/validate-input.d.ts +0 -10
- package/lib/esm/simulate/pipeline/validate-input.js +0 -52
|
@@ -1,111 +1,182 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ExternalServiceError,
|
|
3
|
-
import {
|
|
4
|
-
import { parseTransfers } from "../parsing/index.js";
|
|
1
|
+
import { BaseError, BlockNotFoundError, ExecutionRevertedError, InsufficientFundsError, numberToHex, } from "viem";
|
|
2
|
+
import { ExternalServiceError, InvalidSimulationResponseError, SimulationPackageError, SimulationRevertedError, } from "../../errors.js";
|
|
3
|
+
import { parseSimulationResponse } from "./parse-response.js";
|
|
5
4
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* Runs with `traceTransfers` enabled, so the node synthesizes native-ETH moves
|
|
12
|
-
* (the top-level `value` *and* ETH moved through internal calls — e.g. a
|
|
13
|
-
* `WETH.withdraw` refund or a swap that pays out ETH) as `Transfer` events from
|
|
14
|
-
* the native sentinel. Derives `assetChanges` (net per-token deltas grouped by
|
|
15
|
-
* account) entirely from the emitted transfer logs, with native ETH normalized
|
|
16
|
-
* to viem's `ethAddress`. Top-level `value` is intentionally *not* added on top:
|
|
17
|
-
* `traceTransfers` already logs it, so adding it would double-count.
|
|
5
|
+
* Classify a node-level revert: viem's `ExecutionRevertedError`, or a raw
|
|
6
|
+
* JSON-RPC error — code 3 for "execution reverted", plus an "insufficient
|
|
7
|
+
* funds" message for an unfundable `value` transfer under real native
|
|
8
|
+
* funding (the numeric code varies by node, so the message is matched).
|
|
18
9
|
*/
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
10
|
+
const isNodeRevert = (error) => error instanceof ExecutionRevertedError ||
|
|
11
|
+
error instanceof InsufficientFundsError ||
|
|
12
|
+
(error instanceof Error &&
|
|
13
|
+
(("code" in error && error.code === 3) ||
|
|
14
|
+
/insufficient funds/i.test(error.message)));
|
|
15
|
+
/**
|
|
16
|
+
* Trim a caught error to a safe message: viem's `shortMessage` drops the
|
|
17
|
+
* URL/request-body details its `message` embeds. The original error is always
|
|
18
|
+
* kept as `cause` for logging.
|
|
19
|
+
*/
|
|
20
|
+
const safeMessage = (error) => error instanceof BaseError ? error.shortMessage : String(error);
|
|
21
|
+
/** Map a caught error to the boundary's typed failure for `label`. */
|
|
22
|
+
const toBoundaryError = (label, error) => {
|
|
23
|
+
if (error instanceof SimulationPackageError)
|
|
24
|
+
return error;
|
|
25
|
+
// A node-level revert is a property of the bundle, not the backend. The
|
|
26
|
+
// execution-stage context requires an operation-keyed subject and a
|
|
27
|
+
// node-level revert precedes operation decoding, so no context attaches.
|
|
28
|
+
// `details` carries only the URL-free code/shortMessage; the raw viem
|
|
29
|
+
// error (which embeds the RPC URL) is kept as `cause` only.
|
|
30
|
+
if (label === "eth_simulateV1" && isNodeRevert(error)) {
|
|
31
|
+
const reverted = new SimulationRevertedError(error instanceof BaseError
|
|
32
|
+
? error.details || error.shortMessage
|
|
33
|
+
: error.message, {
|
|
34
|
+
code: "code" in error && typeof error.code !== "undefined"
|
|
35
|
+
? error.code
|
|
36
|
+
: undefined,
|
|
37
|
+
shortMessage: error instanceof BaseError ? error.shortMessage : error.message,
|
|
38
|
+
}, "UNKNOWN_REVERT");
|
|
39
|
+
reverted.cause = error;
|
|
40
|
+
return reverted;
|
|
36
41
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
? { blockNumber }
|
|
44
|
-
: blockNumber !== undefined
|
|
45
|
-
? { blockTag: blockNumber }
|
|
46
|
-
: {};
|
|
42
|
+
return new ExternalServiceError(`${label} error: ${safeMessage(error)}`, {
|
|
43
|
+
cause: error,
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
/** Run one RPC call; anything thrown becomes a typed boundary error. @internal */
|
|
47
|
+
export const rpc = async (label, call) => {
|
|
47
48
|
try {
|
|
48
|
-
|
|
49
|
-
account: sender,
|
|
50
|
-
calls,
|
|
51
|
-
...blockParam,
|
|
52
|
-
// Synthesize native-ETH moves (top-level value + internal calls) as
|
|
53
|
-
// Transfer logs from the native sentinel, so `parseTransfers` captures
|
|
54
|
-
// ETH that emits no real log (e.g. a WETH.withdraw refund).
|
|
55
|
-
traceTransfers: true,
|
|
56
|
-
// Inflate sender ETH balance to prevent false "insufficient gas" reverts.
|
|
57
|
-
// Without this, valid ERC20 flows fail when the sender has low ETH.
|
|
58
|
-
// Use half of uint256 (not the ceiling) so the override leaves headroom
|
|
59
|
-
// for inbound native ETH — e.g. a WETH.withdraw refund or a swap payout
|
|
60
|
-
// would overflow the recipient balance and revert the value transfer if
|
|
61
|
-
// the sender were pinned at maxUint256.
|
|
62
|
-
stateOverrides: [{ address: sender, balance: maxUint256 / 2n }],
|
|
63
|
-
});
|
|
64
|
-
const results = simulationResult.results;
|
|
65
|
-
if (!Array.isArray(results)) {
|
|
66
|
-
throw new ExternalServiceError("eth_simulateV1 returned unexpected response format");
|
|
67
|
-
}
|
|
68
|
-
const failedResult = results.find((r) => r.status !== "success");
|
|
69
|
-
if (failedResult) {
|
|
70
|
-
throw new SimulationRevertedError(failedResult.error?.message ?? "Simulation failed", results);
|
|
71
|
-
}
|
|
72
|
-
const rawCalls = results.map((r) => ({
|
|
73
|
-
logs: (r.logs ?? []).map((log) => ({
|
|
74
|
-
address: log.address,
|
|
75
|
-
topics: [...log.topics],
|
|
76
|
-
data: log.data ?? "0x",
|
|
77
|
-
})),
|
|
78
|
-
status: r.status === "success",
|
|
79
|
-
returnData: r.data ?? "0x",
|
|
80
|
-
gasUsed: r.gasUsed,
|
|
81
|
-
}));
|
|
82
|
-
return {
|
|
83
|
-
calls: rawCalls,
|
|
84
|
-
assetChanges: toAssetChanges(parseTransfers(rawCalls, { wNative })),
|
|
85
|
-
};
|
|
49
|
+
return await call();
|
|
86
50
|
}
|
|
87
51
|
catch (error) {
|
|
88
|
-
|
|
89
|
-
throw error;
|
|
90
|
-
if (error instanceof ExternalServiceError)
|
|
91
|
-
throw error;
|
|
92
|
-
// A node-level "execution reverted" is a property of the bundle, not the backend.
|
|
93
|
-
if (error instanceof ExecutionRevertedError)
|
|
94
|
-
throw new SimulationRevertedError(error.shortMessage, error);
|
|
95
|
-
throw new ExternalServiceError(`eth_simulateV1 error: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
52
|
+
throw toBoundaryError(label, error);
|
|
96
53
|
}
|
|
54
|
+
};
|
|
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
|
+
});
|
|
97
73
|
}
|
|
98
74
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
75
|
+
* 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.
|
|
81
|
+
*
|
|
82
|
+
* The boundary performs these steps:
|
|
83
|
+
*
|
|
84
|
+
* 1. **`eth_simulateV1`** — one `blockStateCalls` entry carrying the planned
|
|
85
|
+
* calls with their per-call `from`, `traceTransfers: true` so the node
|
|
86
|
+
* synthesizes native-ETH moves as transfer logs, and
|
|
87
|
+
* `validation: false`. Validation-off means gas is not charged, which is
|
|
88
|
+
* how gas is separated from economic effects. **No balance override is
|
|
89
|
+
* applied** — `value` transfers are funded by the sender's real native
|
|
90
|
+
* balance.
|
|
91
|
+
* 2. **Response validation** — the response is parsed before the reorg
|
|
92
|
+
* re-fetch so revert/mismatch evidence already in hand surfaces instead
|
|
93
|
+
* of being downgraded to a bypassable {@link ExternalServiceError} by a
|
|
94
|
+
* failing re-fetch. The simulated block must be exactly
|
|
95
|
+
* `stateBlockNumber` or `stateBlockNumber + 1`: geth-style nodes report
|
|
96
|
+
* the former's successor while Anvil reports the pinned block itself.
|
|
97
|
+
* The result records whatever the node returns; consumers must read
|
|
98
|
+
* {@link ExecutionBlock.blockNumber} and never assume +1.
|
|
99
|
+
* 3. **Reorg check** — the pinned state block is re-fetched last to detect a reorg
|
|
100
|
+
* that swapped its hash mid-flight (`InvalidSimulationResponseError`).
|
|
101
|
+
*
|
|
102
|
+
* The endpoint must support `eth_simulateV1` with per-call `from`; there is
|
|
103
|
+
* no fallback backend.
|
|
104
|
+
*
|
|
105
|
+
* @param params - Shared simulation client, execution plan, and already-pinned
|
|
106
|
+
* state block.
|
|
107
|
+
* @returns Deep-frozen {@link SimulationExecution} — per-transaction call
|
|
108
|
+
* results and the resolved {@link ExecutionBlock}.
|
|
109
|
+
* @throws {ExternalServiceError} For transport failures, timeouts,
|
|
110
|
+
* or malformed JSON-RPC envelopes.
|
|
111
|
+
* @throws {InvalidSimulationResponseError} For a response that cannot be
|
|
112
|
+
* trusted (bad shape, call-count mismatch, block
|
|
113
|
+
* other than the pinned state block or its successor, a block timestamp
|
|
114
|
+
* earlier than the pinned block's, a per-call result that fails normalization,
|
|
115
|
+
* or a state-block hash that changed or is no longer served mid-flight).
|
|
116
|
+
* @throws {MissingVerificationEvidenceError} When a planned state read fails.
|
|
117
|
+
* @throws {SimulationRevertedError} When a preparation or user transaction
|
|
118
|
+
* reverts or the node reports a bundle-level revert (code 3 / insufficient
|
|
119
|
+
* funds).
|
|
120
|
+
* @internal
|
|
103
121
|
*/
|
|
104
|
-
function
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
122
|
+
export async function executePlan(params) {
|
|
123
|
+
const { client, plan, stateBlock } = params;
|
|
124
|
+
const response = await rpc("eth_simulateV1", () => client.request({
|
|
125
|
+
method: "eth_simulateV1",
|
|
126
|
+
params: [
|
|
127
|
+
{
|
|
128
|
+
blockStateCalls: [
|
|
129
|
+
{
|
|
130
|
+
calls: plan.calls.map((call) => ({
|
|
131
|
+
from: call.transaction.from,
|
|
132
|
+
to: call.transaction.to,
|
|
133
|
+
data: call.transaction.data,
|
|
134
|
+
value: numberToHex(call.transaction.value),
|
|
135
|
+
})),
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
traceTransfers: true,
|
|
139
|
+
validation: false,
|
|
140
|
+
},
|
|
141
|
+
numberToHex(stateBlock.number),
|
|
142
|
+
],
|
|
143
|
+
}));
|
|
144
|
+
// Response parsing is validation, not transport — it runs before the
|
|
145
|
+
// reorg re-fetch so evidence already in hand reaches the caller as
|
|
146
|
+
// InvalidSimulationResponseError / SimulationRevertedError instead of
|
|
147
|
+
// being downgraded to a bypassable ExternalServiceError by a failing
|
|
148
|
+
// re-fetch.
|
|
149
|
+
const execution = parseSimulationResponse({
|
|
150
|
+
plan,
|
|
151
|
+
blocks: response,
|
|
152
|
+
stateBlockNumber: stateBlock.number,
|
|
153
|
+
stateBlockHash: stateBlock.hash,
|
|
154
|
+
stateBlockTimestamp: stateBlock.timestamp,
|
|
155
|
+
});
|
|
156
|
+
// Reorg window: the pinned state block must still carry the same hash
|
|
157
|
+
// after simulation, or the result may describe a different chain tip.
|
|
158
|
+
const stateBlockAfter = await rpc("eth_getBlock", () => client.getBlock({ blockNumber: stateBlock.number })).catch((error) => {
|
|
159
|
+
if (error instanceof ExternalServiceError &&
|
|
160
|
+
error.cause instanceof BlockNotFoundError)
|
|
161
|
+
return error;
|
|
162
|
+
throw error;
|
|
163
|
+
});
|
|
164
|
+
if (stateBlockAfter instanceof ExternalServiceError ||
|
|
165
|
+
stateBlockAfter.hash !== stateBlock.hash) {
|
|
166
|
+
const after = stateBlockAfter instanceof ExternalServiceError
|
|
167
|
+
? "unavailable"
|
|
168
|
+
: stateBlockAfter.hash;
|
|
169
|
+
throw new InvalidSimulationResponseError(`State block ${stateBlock.number} hash changed during simulation (reorg): ${stateBlock.hash} became ${after}. Re-submit the simulation.`, {
|
|
170
|
+
context: {
|
|
171
|
+
stage: "transport",
|
|
172
|
+
chainId: plan.request.chainId,
|
|
173
|
+
mode: plan.request.mode,
|
|
174
|
+
blockNumber: stateBlock.number,
|
|
175
|
+
},
|
|
176
|
+
...(stateBlockAfter instanceof ExternalServiceError && {
|
|
177
|
+
cause: stateBlockAfter.cause,
|
|
178
|
+
}),
|
|
179
|
+
});
|
|
109
180
|
}
|
|
110
|
-
return
|
|
181
|
+
return execution;
|
|
111
182
|
}
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export { simulateTenderlyRpc } from "./tenderly-rpc.js";
|
|
1
|
+
export { assertEndpointChain, executePlan } from "./eth-simulate-v1.js";
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export { simulateTenderlyRpc } from "./tenderly-rpc.js";
|
|
1
|
+
export { assertEndpointChain, executePlan } from "./eth-simulate-v1.js";
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type Hex } from "viem";
|
|
2
|
+
import type { SimulationCall } from "../../types.js";
|
|
3
|
+
import type { ExecutionPlan, PlannedCall } from "../plan/plan-execution.js";
|
|
4
|
+
import type { StateRead } from "../state/contract.js";
|
|
5
|
+
/** The block coordinates a simulation was pinned to and executed under.
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export interface ExecutionBlock {
|
|
9
|
+
readonly chainId: number;
|
|
10
|
+
readonly stateBlockNumber: bigint;
|
|
11
|
+
readonly stateBlockHash: Hex;
|
|
12
|
+
readonly stateBlockTimestamp: bigint;
|
|
13
|
+
readonly blockNumber: bigint;
|
|
14
|
+
readonly blockTimestamp: bigint;
|
|
15
|
+
}
|
|
16
|
+
/** A planned call and its normalized result. Only successful calls are carried.
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
interface ExecutedCall {
|
|
20
|
+
readonly planned: PlannedCall;
|
|
21
|
+
readonly result: SimulationCall;
|
|
22
|
+
}
|
|
23
|
+
/** One executed state read with its raw return data. @internal */
|
|
24
|
+
interface ExecutedStateRead {
|
|
25
|
+
readonly phase: "before" | "after";
|
|
26
|
+
readonly read: StateRead;
|
|
27
|
+
readonly returnData: Hex;
|
|
28
|
+
}
|
|
29
|
+
/** The output of {@link parseSimulationResponse}: the plan, its block, successful calls and state reads.
|
|
30
|
+
* @internal
|
|
31
|
+
*/
|
|
32
|
+
export interface SimulationExecution {
|
|
33
|
+
readonly plan: ExecutionPlan;
|
|
34
|
+
readonly block: ExecutionBlock;
|
|
35
|
+
readonly calls: readonly ExecutedCall[];
|
|
36
|
+
/** Successful state reads, in plan order, tagged by phase. */
|
|
37
|
+
readonly stateReads: readonly ExecutedStateRead[];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Parse a raw `eth_simulateV1` result into a
|
|
41
|
+
* {@link SimulationExecution}.
|
|
42
|
+
*
|
|
43
|
+
* The response must be exactly one block at the pinned state block or its
|
|
44
|
+
* immediate successor. Block advancement is node-specific: geth-style nodes
|
|
45
|
+
* simulate on top of `base + 1` while Anvil reports the base block itself —
|
|
46
|
+
* this parser records whatever the node reports in {@link ExecutionBlock}
|
|
47
|
+
* and rejects any other height. Consumers must read
|
|
48
|
+
* `block.blockNumber`/`block.blockTimestamp` and never assume +1.
|
|
49
|
+
*
|
|
50
|
+
* @param params - The plan, the raw `eth_simulateV1` result, and the
|
|
51
|
+
* resolved state block.
|
|
52
|
+
* @returns Deep-frozen execution: tagged calls plus one
|
|
53
|
+
* {@link ExecutedStateRead} per planned state read.
|
|
54
|
+
* @throws {InvalidSimulationResponseError} When the result is not exactly one
|
|
55
|
+
* block, on a call-count mismatch, a malformed per-call result, or on a
|
|
56
|
+
* simulated block that is neither the pinned state block nor its immediate successor.
|
|
57
|
+
* @throws {SimulationRevertedError} When a preparation or user-transaction
|
|
58
|
+
* call failed; the `details` payload carries tagged user call results only.
|
|
59
|
+
* @throws {MissingVerificationEvidenceError} When a planned state read fails.
|
|
60
|
+
* @internal
|
|
61
|
+
*/
|
|
62
|
+
export declare function parseSimulationResponse(params: {
|
|
63
|
+
readonly plan: ExecutionPlan;
|
|
64
|
+
readonly blocks: unknown;
|
|
65
|
+
readonly stateBlockNumber: bigint;
|
|
66
|
+
readonly stateBlockHash: Hex;
|
|
67
|
+
readonly stateBlockTimestamp: bigint;
|
|
68
|
+
}): SimulationExecution;
|
|
69
|
+
export {};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { deepFreeze } from "@morpho-org/morpho-ts";
|
|
2
|
+
import { isAddress, isHex } from "viem";
|
|
3
|
+
import { InvalidSimulationResponseError, MissingVerificationEvidenceError, SimulationRevertedError, } from "../../errors.js";
|
|
4
|
+
const isQuantity = (value) => typeof value === "string" && /^0x[0-9a-fA-F]+$/.test(value);
|
|
5
|
+
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6
|
+
const readField = (object, key) => Reflect.get(object, key);
|
|
7
|
+
const isSimulateV1Response = (value) => Array.isArray(value) &&
|
|
8
|
+
value.length === 1 &&
|
|
9
|
+
isObject(value[0]) &&
|
|
10
|
+
isQuantity(readField(value[0], "number")) &&
|
|
11
|
+
isQuantity(readField(value[0], "timestamp")) &&
|
|
12
|
+
isHex(readField(value[0], "hash")) &&
|
|
13
|
+
Array.isArray(readField(value[0], "calls"));
|
|
14
|
+
/**
|
|
15
|
+
* Parse a raw `eth_simulateV1` result into a
|
|
16
|
+
* {@link SimulationExecution}.
|
|
17
|
+
*
|
|
18
|
+
* The response must be exactly one block at the pinned state block or its
|
|
19
|
+
* immediate successor. Block advancement is node-specific: geth-style nodes
|
|
20
|
+
* simulate on top of `base + 1` while Anvil reports the base block itself —
|
|
21
|
+
* this parser records whatever the node reports in {@link ExecutionBlock}
|
|
22
|
+
* and rejects any other height. Consumers must read
|
|
23
|
+
* `block.blockNumber`/`block.blockTimestamp` and never assume +1.
|
|
24
|
+
*
|
|
25
|
+
* @param params - The plan, the raw `eth_simulateV1` result, and the
|
|
26
|
+
* resolved state block.
|
|
27
|
+
* @returns Deep-frozen execution: tagged calls plus one
|
|
28
|
+
* {@link ExecutedStateRead} per planned state read.
|
|
29
|
+
* @throws {InvalidSimulationResponseError} When the result is not exactly one
|
|
30
|
+
* block, on a call-count mismatch, a malformed per-call result, or on a
|
|
31
|
+
* simulated block that is neither the pinned state block nor its immediate successor.
|
|
32
|
+
* @throws {SimulationRevertedError} When a preparation or user-transaction
|
|
33
|
+
* call failed; the `details` payload carries tagged user call results only.
|
|
34
|
+
* @throws {MissingVerificationEvidenceError} When a planned state read fails.
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
export function parseSimulationResponse(params) {
|
|
38
|
+
const { plan, blocks } = params;
|
|
39
|
+
const errorContext = {
|
|
40
|
+
stage: "transport",
|
|
41
|
+
chainId: plan.request.chainId,
|
|
42
|
+
mode: plan.request.mode,
|
|
43
|
+
blockNumber: params.stateBlockNumber,
|
|
44
|
+
};
|
|
45
|
+
if (!isSimulateV1Response(blocks)) {
|
|
46
|
+
throw new InvalidSimulationResponseError("eth_simulateV1 returned an unexpected response shape. Check that the configured endpoint implements eth_simulateV1.", { context: errorContext });
|
|
47
|
+
}
|
|
48
|
+
const block = blocks[0];
|
|
49
|
+
const blockNumber = BigInt(block.number);
|
|
50
|
+
const blockTimestamp = BigInt(block.timestamp);
|
|
51
|
+
if (blockNumber !== params.stateBlockNumber &&
|
|
52
|
+
blockNumber !== params.stateBlockNumber + 1n) {
|
|
53
|
+
throw new InvalidSimulationResponseError(`eth_simulateV1 reported block ${blockNumber} but the pinned state block is ${params.stateBlockNumber}; the node did not honor the pinned block.`, { context: errorContext });
|
|
54
|
+
}
|
|
55
|
+
// Anvil re-hashes the pinned block, so only the geth-style successor can be
|
|
56
|
+
// pinned by hash: it must report a parentHash (geth always does) equal to
|
|
57
|
+
// the pinned state block hash.
|
|
58
|
+
if (blockNumber === params.stateBlockNumber + 1n &&
|
|
59
|
+
block.parentHash !== params.stateBlockHash) {
|
|
60
|
+
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
|
+
}
|
|
62
|
+
if (blockTimestamp < params.stateBlockTimestamp) {
|
|
63
|
+
throw new InvalidSimulationResponseError(`eth_simulateV1 reported block timestamp ${blockTimestamp}, behind the pinned state block timestamp ${params.stateBlockTimestamp}. Check that the endpoint executes on top of the requested block.`, { context: errorContext });
|
|
64
|
+
}
|
|
65
|
+
if (block.calls.length !== plan.calls.length) {
|
|
66
|
+
throw new InvalidSimulationResponseError(`eth_simulateV1 returned ${block.calls.length} call result(s) for ${plan.calls.length} planned call(s). Refusing to map the response with mismatched lengths.`, { context: errorContext });
|
|
67
|
+
}
|
|
68
|
+
const calls = block.calls.map((call, index) => {
|
|
69
|
+
const planned = plan.calls[index];
|
|
70
|
+
let result;
|
|
71
|
+
try {
|
|
72
|
+
if (!isObject(call) ||
|
|
73
|
+
(call.status !== "0x0" && call.status !== "0x1") ||
|
|
74
|
+
!isHex(call.returnData) ||
|
|
75
|
+
!isQuantity(call.gasUsed) ||
|
|
76
|
+
(call.logs != null && !Array.isArray(call.logs)))
|
|
77
|
+
throw new InvalidSimulationResponseError(`eth_simulateV1 returned a malformed call result at position ${index}.`, { context: errorContext });
|
|
78
|
+
result = {
|
|
79
|
+
logs: (call.logs ?? []).map((log) => {
|
|
80
|
+
if (!isAddress(log.address) ||
|
|
81
|
+
!Array.isArray(log.topics) ||
|
|
82
|
+
log.topics.some((topic) => !isHex(topic)) ||
|
|
83
|
+
(log.data !== undefined && !isHex(log.data)))
|
|
84
|
+
throw new InvalidSimulationResponseError(`eth_simulateV1 returned a malformed log at call position ${index}.`, { context: errorContext });
|
|
85
|
+
return {
|
|
86
|
+
address: log.address,
|
|
87
|
+
topics: log.topics,
|
|
88
|
+
data: log.data ?? "0x",
|
|
89
|
+
};
|
|
90
|
+
}),
|
|
91
|
+
status: call.status === "0x1",
|
|
92
|
+
returnData: call.returnData,
|
|
93
|
+
gasUsed: BigInt(call.gasUsed),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
catch (cause) {
|
|
97
|
+
if (cause instanceof InvalidSimulationResponseError)
|
|
98
|
+
throw cause;
|
|
99
|
+
throw new InvalidSimulationResponseError(`eth_simulateV1 returned a malformed call result at position ${index}.`, { context: errorContext, cause });
|
|
100
|
+
}
|
|
101
|
+
return { planned, result, call };
|
|
102
|
+
});
|
|
103
|
+
// A preparation call that reverted did not produce its promised state —
|
|
104
|
+
// it is reported before user-transaction reverts it caused.
|
|
105
|
+
const failedPreparation = calls.find((entry) => entry.planned.type === "preparation" && !entry.result.status);
|
|
106
|
+
if (failedPreparation) {
|
|
107
|
+
const message = failedPreparation.call.error?.message;
|
|
108
|
+
throw new SimulationRevertedError(`Authorization preparation call failed during simulation${message !== undefined ? `: ${message}` : ""}. Re-submit the bundle; if it persists, check that the endpoint executes preparation calls.`, undefined, "UNKNOWN_REVERT", {
|
|
109
|
+
stage: "preparation",
|
|
110
|
+
chainId: plan.request.chainId,
|
|
111
|
+
mode: plan.request.mode,
|
|
112
|
+
blockNumber: params.stateBlockNumber,
|
|
113
|
+
authorizationIndex: failedPreparation.planned.authorizationIndex,
|
|
114
|
+
preparationCallIndex: failedPreparation.planned.callIndex,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
// A user-transaction revert belongs to the bundle, not the boundary.
|
|
118
|
+
const failedUserCall = calls.find(({ planned, result }) => planned.type === "transaction" && !result.status);
|
|
119
|
+
if (failedUserCall && failedUserCall.planned.type === "transaction") {
|
|
120
|
+
throw new SimulationRevertedError(failedUserCall.call.error?.message ?? "Simulation failed", deepFreeze(calls
|
|
121
|
+
.filter((entry) => entry.planned.type === "transaction")
|
|
122
|
+
.map(({ planned, result }) => ({
|
|
123
|
+
transactionIndex: planned.transactionIndex,
|
|
124
|
+
result,
|
|
125
|
+
}))), "UNKNOWN_REVERT");
|
|
126
|
+
}
|
|
127
|
+
const stateReads = [];
|
|
128
|
+
for (const { planned, result, call } of calls) {
|
|
129
|
+
if (planned.type !== "stateRead")
|
|
130
|
+
continue;
|
|
131
|
+
if (!result.status) {
|
|
132
|
+
const message = call.error?.message;
|
|
133
|
+
throw new MissingVerificationEvidenceError(`State read "${planned.read.id}" failed during simulation${message !== undefined ? `: ${message}` : ""}. Re-submit the bundle; if it persists, check that the endpoint executes view calls in the same block.`, {
|
|
134
|
+
context: {
|
|
135
|
+
stage: "verification",
|
|
136
|
+
chainId: plan.request.chainId,
|
|
137
|
+
mode: plan.request.mode,
|
|
138
|
+
blockNumber: params.stateBlockNumber,
|
|
139
|
+
field: planned.read.id,
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
stateReads.push({
|
|
144
|
+
phase: planned.phase,
|
|
145
|
+
read: planned.read,
|
|
146
|
+
returnData: call.returnData,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return deepFreeze({
|
|
150
|
+
plan,
|
|
151
|
+
block: {
|
|
152
|
+
chainId: plan.request.chainId,
|
|
153
|
+
stateBlockNumber: params.stateBlockNumber,
|
|
154
|
+
stateBlockHash: params.stateBlockHash,
|
|
155
|
+
stateBlockTimestamp: params.stateBlockTimestamp,
|
|
156
|
+
blockNumber,
|
|
157
|
+
blockTimestamp,
|
|
158
|
+
},
|
|
159
|
+
// Every failure was classified above: only successful calls remain.
|
|
160
|
+
calls: calls
|
|
161
|
+
.filter((entry) => entry.result.status)
|
|
162
|
+
.map(({ planned, result }) => ({ planned, result })),
|
|
163
|
+
stateReads,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type Address, type Client } from "viem";
|
|
2
|
+
import type { OperationLimit } from "../../limits.js";
|
|
3
|
+
import type { SimulationMode } from "../../params.js";
|
|
4
|
+
/** Caller-selected operation with only the quoted wallet assets resolved. @internal */
|
|
5
|
+
export interface ResolvedSlippageOperation {
|
|
6
|
+
readonly limit: OperationLimit;
|
|
7
|
+
readonly assetsPaid?: Address;
|
|
8
|
+
readonly assetsReceived?: Address;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Resolve underlying tokens only when an asset quote lacks an explicit token.
|
|
12
|
+
* Share-only quotes need no RPC reads; vault versions and factories are not discovered.
|
|
13
|
+
* @param params - Caller operations, Morpho address, client, request context, and pinned state block.
|
|
14
|
+
* @returns Operations with the token addresses required by their asset quotes.
|
|
15
|
+
* @throws {ExternalServiceError} For transport failures during a required metadata read.
|
|
16
|
+
* @throws {MissingVerificationEvidenceError} For reverted, malformed, or empty metadata.
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveAssets(params: {
|
|
20
|
+
readonly client: Client;
|
|
21
|
+
readonly morpho: Address;
|
|
22
|
+
readonly operations: readonly OperationLimit[];
|
|
23
|
+
readonly chainId: number;
|
|
24
|
+
readonly mode: SimulationMode;
|
|
25
|
+
readonly blockNumber: bigint;
|
|
26
|
+
}): Promise<readonly ResolvedSlippageOperation[]>;
|