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