@morpho-org/evm-simulation 1.0.0 → 2.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 +14 -4
- package/lib/cjs/errors.d.ts +3 -3
- package/lib/cjs/errors.js +2 -3
- package/lib/cjs/index.d.ts +2 -2
- package/lib/cjs/index.js +9 -9
- package/lib/cjs/screen-addresses/screen-addresses.d.ts +37 -21
- package/lib/cjs/screen-addresses/screen-addresses.js +40 -21
- package/lib/cjs/simulate/backends/eth-simulate-v1.js +11 -11
- package/lib/cjs/simulate/backends/tenderly-rest.js +62 -52
- package/lib/cjs/simulate/parsing/transfers.d.ts +21 -9
- package/lib/cjs/simulate/parsing/transfers.js +141 -77
- package/lib/cjs/simulate/pipeline/bundler-retention.js +1 -0
- package/lib/cjs/simulate/simulate.d.ts +45 -6
- package/lib/cjs/simulate/simulate.js +52 -8
- package/lib/cjs/types.d.ts +81 -25
- package/lib/esm/errors.d.ts +3 -3
- package/lib/esm/errors.js +2 -3
- package/lib/esm/index.d.ts +2 -2
- package/lib/esm/index.js +2 -2
- package/lib/esm/screen-addresses/screen-addresses.d.ts +37 -21
- package/lib/esm/screen-addresses/screen-addresses.js +40 -21
- package/lib/esm/simulate/backends/eth-simulate-v1.js +12 -12
- package/lib/esm/simulate/backends/tenderly-rest.js +62 -52
- package/lib/esm/simulate/parsing/transfers.d.ts +21 -9
- package/lib/esm/simulate/parsing/transfers.js +141 -77
- package/lib/esm/simulate/pipeline/bundler-retention.js +2 -1
- package/lib/esm/simulate/simulate.d.ts +45 -6
- package/lib/esm/simulate/simulate.js +52 -8
- package/lib/esm/types.d.ts +81 -25
- package/package.json +6 -6
- package/src/index.ts +13 -12
package/README.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# @morpho-org/evm-simulation
|
|
2
2
|
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
3
5
|
EVM transaction simulation engine for Morpho — bundle execution preview,
|
|
4
6
|
transfer parsing, and compliance screening.
|
|
5
7
|
|
|
6
|
-
##
|
|
8
|
+
## Installation
|
|
7
9
|
|
|
8
10
|
Add to the consuming app's `package.json`:
|
|
9
11
|
|
|
@@ -15,7 +17,7 @@ Add to the consuming app's `package.json`:
|
|
|
15
17
|
}
|
|
16
18
|
```
|
|
17
19
|
|
|
18
|
-
##
|
|
20
|
+
## Usage
|
|
19
21
|
|
|
20
22
|
```ts
|
|
21
23
|
import {
|
|
@@ -60,7 +62,7 @@ try {
|
|
|
60
62
|
}
|
|
61
63
|
```
|
|
62
64
|
|
|
63
|
-
|
|
65
|
+
### API surface
|
|
64
66
|
|
|
65
67
|
All symbols below are re-exported from the package root.
|
|
66
68
|
|
|
@@ -70,8 +72,16 @@ All symbols below are re-exported from the package root.
|
|
|
70
72
|
- Data types: `SimulationTransaction`, `SimulationAuthorization`, `Transfer`, `SimulateParams`, `SimulationResult`.
|
|
71
73
|
- Errors: `SimulationPackageError` (abstract base — `instanceof` it to catch any package error), `SimulationRevertedError`, `BlacklistViolationError`, `AddressScreeningError`, `ExternalServiceError`, `SimulationValidationError`, `UnsupportedChainError`.
|
|
72
74
|
|
|
73
|
-
|
|
75
|
+
### Deeper docs
|
|
74
76
|
|
|
75
77
|
See [`CLAUDE.md`](./CLAUDE.md) in this directory for the execution flow diagram,
|
|
76
78
|
backend tradeoffs, authorizations model, screening design, error-handling table,
|
|
77
79
|
and recipes for adding a chain or a new backend.
|
|
80
|
+
|
|
81
|
+
## Development
|
|
82
|
+
|
|
83
|
+
Contribute from the monorepo root. See [CONTRIBUTING.md](../../CONTRIBUTING.md) for setup, checks, and package workflow. Report vulnerabilities through [SECURITY.md](../../SECURITY.md).
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
MIT. See [LICENSE](./LICENSE).
|
package/lib/cjs/errors.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Base
|
|
3
|
-
*
|
|
4
|
-
* Consumers use try/catch + instanceof to handle specific error types.
|
|
2
|
+
* Base class for every error this package throws. Transport-agnostic — no HTTP status codes.
|
|
3
|
+
* Consumers pattern-match with `instanceof` on the concrete subclass.
|
|
5
4
|
*/
|
|
6
5
|
export declare abstract class SimulationPackageError extends Error {
|
|
6
|
+
/** Stable string discriminator for log aggregation and external mapping. */
|
|
7
7
|
abstract readonly code: string;
|
|
8
8
|
constructor(message: string, options?: ErrorOptions);
|
|
9
9
|
}
|
package/lib/cjs/errors.js
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.UnsupportedChainError = exports.SimulationValidationError = exports.ExternalServiceError = exports.AddressScreeningError = exports.BlacklistViolationError = exports.SimulationRevertedError = exports.SimulationPackageError = void 0;
|
|
4
4
|
/**
|
|
5
|
-
* Base
|
|
6
|
-
*
|
|
7
|
-
* Consumers use try/catch + instanceof to handle specific error types.
|
|
5
|
+
* Base class for every error this package throws. Transport-agnostic — no HTTP status codes.
|
|
6
|
+
* Consumers pattern-match with `instanceof` on the concrete subclass.
|
|
8
7
|
*/
|
|
9
8
|
class SimulationPackageError extends Error {
|
|
10
9
|
constructor(message, options) {
|
package/lib/cjs/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
+
export { AddressScreeningError, BlacklistViolationError, ExternalServiceError, SimulationPackageError, SimulationRevertedError, SimulationValidationError, UnsupportedChainError, } from "./errors.js";
|
|
1
2
|
export { screenAddresses } from "./screen-addresses/index.js";
|
|
2
3
|
export { simulate } from "./simulate/index.js";
|
|
3
|
-
export type { ChainSimulationConfig, SimulateParams, SimulationAuthorization, SimulationConfig, SimulationLogger, SimulationResult, SimulationTransaction, TenderlyRestConfig, Transfer, } from "./types.js";
|
|
4
|
-
export { SimulationPackageError, SimulationRevertedError, BlacklistViolationError, AddressScreeningError, ExternalServiceError, SimulationValidationError, UnsupportedChainError, } from "./errors.js";
|
|
4
|
+
export type { ChainSimulationConfig, RawLog, SimulateParams, SimulationAuthorization, SimulationCall, SimulationConfig, SimulationLogger, SimulationResult, SimulationTransaction, TenderlyRestConfig, Transfer, } from "./types.js";
|
package/lib/cjs/index.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.UnsupportedChainError = exports.SimulationValidationError = exports.ExternalServiceError = exports.AddressScreeningError = exports.BlacklistViolationError = exports.SimulationRevertedError = exports.SimulationPackageError = exports.simulate = exports.screenAddresses = void 0;
|
|
4
2
|
// Public fns (via feature-folder barrels)
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
var index_js_2 = require("./simulate/index.js");
|
|
8
|
-
Object.defineProperty(exports, "simulate", { enumerable: true, get: function () { return index_js_2.simulate; } });
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.simulate = exports.screenAddresses = exports.UnsupportedChainError = exports.SimulationValidationError = exports.SimulationRevertedError = exports.SimulationPackageError = exports.ExternalServiceError = exports.BlacklistViolationError = exports.AddressScreeningError = void 0;
|
|
9
5
|
// Errors (for instanceof checks by consumers)
|
|
10
6
|
var errors_js_1 = require("./errors.js");
|
|
11
|
-
Object.defineProperty(exports, "SimulationPackageError", { enumerable: true, get: function () { return errors_js_1.SimulationPackageError; } });
|
|
12
|
-
Object.defineProperty(exports, "SimulationRevertedError", { enumerable: true, get: function () { return errors_js_1.SimulationRevertedError; } });
|
|
13
|
-
Object.defineProperty(exports, "BlacklistViolationError", { enumerable: true, get: function () { return errors_js_1.BlacklistViolationError; } });
|
|
14
7
|
Object.defineProperty(exports, "AddressScreeningError", { enumerable: true, get: function () { return errors_js_1.AddressScreeningError; } });
|
|
8
|
+
Object.defineProperty(exports, "BlacklistViolationError", { enumerable: true, get: function () { return errors_js_1.BlacklistViolationError; } });
|
|
15
9
|
Object.defineProperty(exports, "ExternalServiceError", { enumerable: true, get: function () { return errors_js_1.ExternalServiceError; } });
|
|
10
|
+
Object.defineProperty(exports, "SimulationPackageError", { enumerable: true, get: function () { return errors_js_1.SimulationPackageError; } });
|
|
11
|
+
Object.defineProperty(exports, "SimulationRevertedError", { enumerable: true, get: function () { return errors_js_1.SimulationRevertedError; } });
|
|
16
12
|
Object.defineProperty(exports, "SimulationValidationError", { enumerable: true, get: function () { return errors_js_1.SimulationValidationError; } });
|
|
17
13
|
Object.defineProperty(exports, "UnsupportedChainError", { enumerable: true, get: function () { return errors_js_1.UnsupportedChainError; } });
|
|
14
|
+
var index_js_1 = require("./screen-addresses/index.js");
|
|
15
|
+
Object.defineProperty(exports, "screenAddresses", { enumerable: true, get: function () { return index_js_1.screenAddresses; } });
|
|
16
|
+
var index_js_2 = require("./simulate/index.js");
|
|
17
|
+
Object.defineProperty(exports, "simulate", { enumerable: true, get: function () { return index_js_2.simulate; } });
|
|
@@ -1,29 +1,45 @@
|
|
|
1
1
|
import type { SimulationLogger, SimulationTransaction, Transfer } from "../types.js";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Screens every address that appears in a simulated bundle (transfer senders and recipients,
|
|
4
|
+
* plus tx `from` / `to`) against the static {@link sanctionedAddresses} list and, when
|
|
5
|
+
* configured, the Chainalysis Entity API.
|
|
4
6
|
*
|
|
5
|
-
*
|
|
6
|
-
* - Transfer senders and recipients from simulation result
|
|
7
|
-
* - `from` and `to` of every tx in `simulationTxs` (catches native-value flows)
|
|
7
|
+
* Addresses are deduped and lowercased; the zero address is removed. Checks run in order:
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* **Fail-closed on
|
|
18
|
-
*
|
|
19
|
-
* The data already persisted server-side, so failing open at this point
|
|
20
|
-
* would let a pass slip through based on a half-completed check.
|
|
21
|
-
* **Fail-closed on schema drift**: if Chainalysis returns an unknown risk
|
|
22
|
-
* tier or a body that doesn't parse, the address is treated as Severe so a
|
|
23
|
-
* future stricter tier isn't silently downgraded.
|
|
9
|
+
* 1. **Static sanctioned list** — always on, no network. Throws on the first hit.
|
|
10
|
+
* 2. **Chainalysis Entity API** — one lookup per address, lookups parallelized. Only runs when
|
|
11
|
+
* `chainalysisApiKey` is provided.
|
|
12
|
+
* - **Fail-open on transport errors before registration** (network / timeout / non-ok POST):
|
|
13
|
+
* treat the address as clean and log a warn. A Chainalysis outage must not block all
|
|
14
|
+
* simulations.
|
|
15
|
+
* - **Fail-closed on partial registration**: if the registration POST succeeded but the
|
|
16
|
+
* lookup GET fails, treat the address as Severe.
|
|
17
|
+
* - **Fail-closed on schema drift**: an unknown risk tier or unparsed body is treated as
|
|
18
|
+
* Severe so a future stricter tier is not silently downgraded.
|
|
24
19
|
*
|
|
25
|
-
*
|
|
26
|
-
* `
|
|
20
|
+
* Intended to be called after {@link simulate}, passing the returned `simulationTxs` and
|
|
21
|
+
* `transfers`.
|
|
22
|
+
*
|
|
23
|
+
* @param params.simulationTxs - The resolved transactions returned by `simulate()`.
|
|
24
|
+
* @param params.transfers - The parsed transfers returned by `simulate()`.
|
|
25
|
+
* @param params.chainalysisApiKey - Optional Chainalysis token. When absent, only the static
|
|
26
|
+
* sanctioned list runs.
|
|
27
|
+
* @param params.signal - Optional `AbortSignal` propagated to every Chainalysis request.
|
|
28
|
+
* @param params.logger - Optional `SimulationLogger` for warn-level diagnostics.
|
|
29
|
+
* @returns Resolves to `void` on a clean screening; throws on any Severe hit.
|
|
30
|
+
* @throws {AddressScreeningError} when any address matches the static sanctioned list or
|
|
31
|
+
* resolves to Severe via Chainalysis.
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* import { screenAddresses, simulate } from "@morpho-org/evm-simulation";
|
|
35
|
+
*
|
|
36
|
+
* const result = await simulate(config, params);
|
|
37
|
+
* await screenAddresses({
|
|
38
|
+
* simulationTxs: result.simulationTxs,
|
|
39
|
+
* transfers: result.transfers,
|
|
40
|
+
* chainalysisApiKey: process.env.CHAINALYSIS_API_KEY,
|
|
41
|
+
* });
|
|
42
|
+
* ```
|
|
27
43
|
*/
|
|
28
44
|
export declare function screenAddresses(params: {
|
|
29
45
|
simulationTxs: SimulationTransaction[];
|
|
@@ -19,30 +19,46 @@ const chainalysisEntitySchema = zod_1.z
|
|
|
19
19
|
})
|
|
20
20
|
.passthrough();
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
22
|
+
* Screens every address that appears in a simulated bundle (transfer senders and recipients,
|
|
23
|
+
* plus tx `from` / `to`) against the static {@link sanctionedAddresses} list and, when
|
|
24
|
+
* configured, the Chainalysis Entity API.
|
|
23
25
|
*
|
|
24
|
-
*
|
|
25
|
-
* - Transfer senders and recipients from simulation result
|
|
26
|
-
* - `from` and `to` of every tx in `simulationTxs` (catches native-value flows)
|
|
26
|
+
* Addresses are deduped and lowercased; the zero address is removed. Checks run in order:
|
|
27
27
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* **Fail-closed on
|
|
37
|
-
*
|
|
38
|
-
* The data already persisted server-side, so failing open at this point
|
|
39
|
-
* would let a pass slip through based on a half-completed check.
|
|
40
|
-
* **Fail-closed on schema drift**: if Chainalysis returns an unknown risk
|
|
41
|
-
* tier or a body that doesn't parse, the address is treated as Severe so a
|
|
42
|
-
* future stricter tier isn't silently downgraded.
|
|
28
|
+
* 1. **Static sanctioned list** — always on, no network. Throws on the first hit.
|
|
29
|
+
* 2. **Chainalysis Entity API** — one lookup per address, lookups parallelized. Only runs when
|
|
30
|
+
* `chainalysisApiKey` is provided.
|
|
31
|
+
* - **Fail-open on transport errors before registration** (network / timeout / non-ok POST):
|
|
32
|
+
* treat the address as clean and log a warn. A Chainalysis outage must not block all
|
|
33
|
+
* simulations.
|
|
34
|
+
* - **Fail-closed on partial registration**: if the registration POST succeeded but the
|
|
35
|
+
* lookup GET fails, treat the address as Severe.
|
|
36
|
+
* - **Fail-closed on schema drift**: an unknown risk tier or unparsed body is treated as
|
|
37
|
+
* Severe so a future stricter tier is not silently downgraded.
|
|
43
38
|
*
|
|
44
|
-
*
|
|
45
|
-
* `
|
|
39
|
+
* Intended to be called after {@link simulate}, passing the returned `simulationTxs` and
|
|
40
|
+
* `transfers`.
|
|
41
|
+
*
|
|
42
|
+
* @param params.simulationTxs - The resolved transactions returned by `simulate()`.
|
|
43
|
+
* @param params.transfers - The parsed transfers returned by `simulate()`.
|
|
44
|
+
* @param params.chainalysisApiKey - Optional Chainalysis token. When absent, only the static
|
|
45
|
+
* sanctioned list runs.
|
|
46
|
+
* @param params.signal - Optional `AbortSignal` propagated to every Chainalysis request.
|
|
47
|
+
* @param params.logger - Optional `SimulationLogger` for warn-level diagnostics.
|
|
48
|
+
* @returns Resolves to `void` on a clean screening; throws on any Severe hit.
|
|
49
|
+
* @throws {AddressScreeningError} when any address matches the static sanctioned list or
|
|
50
|
+
* resolves to Severe via Chainalysis.
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* import { screenAddresses, simulate } from "@morpho-org/evm-simulation";
|
|
54
|
+
*
|
|
55
|
+
* const result = await simulate(config, params);
|
|
56
|
+
* await screenAddresses({
|
|
57
|
+
* simulationTxs: result.simulationTxs,
|
|
58
|
+
* transfers: result.transfers,
|
|
59
|
+
* chainalysisApiKey: process.env.CHAINALYSIS_API_KEY,
|
|
60
|
+
* });
|
|
61
|
+
* ```
|
|
46
62
|
*/
|
|
47
63
|
async function screenAddresses(params) {
|
|
48
64
|
const { simulationTxs, transfers, chainalysisApiKey, signal, logger } = params;
|
|
@@ -103,7 +119,10 @@ async function screenAddresses(params) {
|
|
|
103
119
|
*
|
|
104
120
|
* A SINGLE deadline covers both the POST and the GET, keeping the per-address
|
|
105
121
|
* wall-time budget at `CHAINALYSIS_TIMEOUT_MS` (5 s) rather than doubling it.
|
|
122
|
+
*
|
|
123
|
+
* @internal
|
|
106
124
|
*/
|
|
125
|
+
// biome-ignore lint/complexity/useMaxParams: TODO refactor to ≤2 params
|
|
107
126
|
async function screenWithChainalysis(address, apiKey, parentSignal) {
|
|
108
127
|
const headers = {
|
|
109
128
|
"Content-Type": "application/json",
|
|
@@ -54,17 +54,17 @@ async function simulateV1(params) {
|
|
|
54
54
|
if (failedResult) {
|
|
55
55
|
throw new errors_js_1.SimulationRevertedError(failedResult.error?.message ?? "Simulation failed", results);
|
|
56
56
|
}
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
return {
|
|
57
|
+
const rawCalls = results.map((r) => ({
|
|
58
|
+
logs: (r.logs ?? []).map((log) => ({
|
|
59
|
+
address: log.address,
|
|
60
|
+
topics: [...log.topics],
|
|
61
|
+
data: log.data ?? "0x",
|
|
62
|
+
})),
|
|
63
|
+
status: r.status === "success",
|
|
64
|
+
returnData: r.data ?? "0x",
|
|
65
|
+
gasUsed: r.gasUsed,
|
|
66
|
+
}));
|
|
67
|
+
return { calls: rawCalls };
|
|
68
68
|
}
|
|
69
69
|
catch (error) {
|
|
70
70
|
if (error instanceof errors_js_1.SimulationRevertedError)
|
|
@@ -22,6 +22,7 @@ const tenderlyRawResponseSchema = zod_1.z
|
|
|
22
22
|
transaction: zod_1.z
|
|
23
23
|
.object({
|
|
24
24
|
error_message: zod_1.z.string().optional(),
|
|
25
|
+
gas_used: zod_1.z.number(),
|
|
25
26
|
transaction_info: zod_1.z
|
|
26
27
|
.object({
|
|
27
28
|
logs: zod_1.z
|
|
@@ -39,6 +40,11 @@ const tenderlyRawResponseSchema = zod_1.z
|
|
|
39
40
|
.passthrough())
|
|
40
41
|
.optional(),
|
|
41
42
|
asset_changes: zod_1.z.unknown().optional(),
|
|
43
|
+
call_trace: zod_1.z
|
|
44
|
+
.object({
|
|
45
|
+
output: hexSchema,
|
|
46
|
+
})
|
|
47
|
+
.passthrough(),
|
|
42
48
|
})
|
|
43
49
|
.passthrough(),
|
|
44
50
|
})
|
|
@@ -101,6 +107,7 @@ function buildBaseUrl(config) {
|
|
|
101
107
|
return `${config.apiBaseUrl}/api/v1/account/${account}/project/${project}`;
|
|
102
108
|
}
|
|
103
109
|
/** Build the `TenderlySimulateRequest` body for one transaction. Shared by single + bundle. */
|
|
110
|
+
// biome-ignore lint/complexity/useMaxParams: TODO refactor to ≤2 params
|
|
104
111
|
function buildTxBody(tx, chainId, shareable, blockNumber) {
|
|
105
112
|
return {
|
|
106
113
|
network_id: String(chainId),
|
|
@@ -132,19 +139,16 @@ async function simulateSingle(params) {
|
|
|
132
139
|
throw new errors_js_1.ExternalServiceError(`Tenderly REST API returned ${response.status}: ${response.statusText}`);
|
|
133
140
|
}
|
|
134
141
|
const data = tenderlyRawResponseSchema.parse(await response.json());
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
result.tenderlyUrl = undefined;
|
|
146
|
-
}
|
|
147
|
-
return result;
|
|
142
|
+
const call = parseTenderlyCall(data);
|
|
143
|
+
const tenderlyUrl = await resolveTenderlyUrl({
|
|
144
|
+
baseUrl,
|
|
145
|
+
config,
|
|
146
|
+
simulationId: data.simulation.id,
|
|
147
|
+
shareable,
|
|
148
|
+
signal,
|
|
149
|
+
logger,
|
|
150
|
+
});
|
|
151
|
+
return { calls: [call], tenderlyUrl };
|
|
148
152
|
}
|
|
149
153
|
async function simulateBundle(params) {
|
|
150
154
|
const { baseUrl, config, chainId, transactions, blockNumber, shareable, signal, logger, } = params;
|
|
@@ -165,39 +169,40 @@ async function simulateBundle(params) {
|
|
|
165
169
|
}
|
|
166
170
|
const data = tenderlyBundleRawResponseSchema.parse(await response.json());
|
|
167
171
|
const simulations = data.simulation_results;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
172
|
+
const calls = simulations.map((sim) => parseTenderlyCall(sim));
|
|
173
|
+
const lastSim = simulations[simulations.length - 1];
|
|
174
|
+
const tenderlyUrl = await resolveTenderlyUrl({
|
|
175
|
+
baseUrl,
|
|
176
|
+
config,
|
|
177
|
+
simulationId: lastSim.simulation.id,
|
|
178
|
+
shareable,
|
|
179
|
+
signal,
|
|
180
|
+
logger,
|
|
181
|
+
});
|
|
182
|
+
return { calls, tenderlyUrl };
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Build the shareable Tenderly dashboard URL for the given simulation, if
|
|
186
|
+
* `shareable=true` and `/share` succeeds. Returns `undefined` otherwise.
|
|
187
|
+
*
|
|
188
|
+
* Mirrors the legacy "build optimistically, clear on failure" pattern: the
|
|
189
|
+
* URL is constructed up-front and cleared if `shareSimulation` returns false.
|
|
190
|
+
*/
|
|
191
|
+
async function resolveTenderlyUrl(params) {
|
|
192
|
+
const { baseUrl, config, simulationId, shareable, signal, logger } = params;
|
|
193
|
+
if (!shareable || !simulationId)
|
|
194
|
+
return undefined;
|
|
195
|
+
let tenderlyUrl = `https://dashboard.tenderly.co/shared/simulation/${simulationId}`;
|
|
196
|
+
const shared = await shareSimulation({
|
|
197
|
+
baseUrl,
|
|
198
|
+
config,
|
|
199
|
+
simulationId,
|
|
200
|
+
signal,
|
|
201
|
+
logger,
|
|
202
|
+
});
|
|
203
|
+
if (!shared)
|
|
204
|
+
tenderlyUrl = undefined;
|
|
205
|
+
return tenderlyUrl;
|
|
201
206
|
}
|
|
202
207
|
/**
|
|
203
208
|
* Try to make a simulation publicly shareable. Returns true iff Tenderly's
|
|
@@ -236,7 +241,13 @@ async function shareSimulation(params) {
|
|
|
236
241
|
return false;
|
|
237
242
|
}
|
|
238
243
|
}
|
|
239
|
-
|
|
244
|
+
/**
|
|
245
|
+
* Parse one Tenderly simulation response into a `RawCall`. The caller
|
|
246
|
+
* (single-sim or bundle) assembles the `RawSimulationResult` around it.
|
|
247
|
+
*
|
|
248
|
+
* Throws `SimulationRevertedError` if `simulation.status !== true`.
|
|
249
|
+
*/
|
|
250
|
+
function parseTenderlyCall(data) {
|
|
240
251
|
const { simulation, transaction } = data;
|
|
241
252
|
if (simulation.status !== true) {
|
|
242
253
|
const errorMessage = transaction.error_message ||
|
|
@@ -258,12 +269,11 @@ function parseTenderlyResponse(data, shareable) {
|
|
|
258
269
|
});
|
|
259
270
|
}
|
|
260
271
|
}
|
|
261
|
-
const tenderlyUrl = shareable && simulation.id
|
|
262
|
-
? `https://dashboard.tenderly.co/shared/simulation/${simulation.id}`
|
|
263
|
-
: undefined;
|
|
264
272
|
return {
|
|
265
273
|
logs: rawLogs,
|
|
266
|
-
|
|
267
|
-
|
|
274
|
+
status: true,
|
|
275
|
+
returnData: transaction.transaction_info.call_trace.output,
|
|
276
|
+
gasUsed: BigInt(transaction.gas_used),
|
|
277
|
+
assetChanges: transaction.transaction_info.asset_changes,
|
|
268
278
|
};
|
|
269
279
|
}
|
|
@@ -1,27 +1,39 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { RawCall, SimulationLogger, Transfer } from "../../types.js";
|
|
2
2
|
export declare const TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
3
3
|
export declare const WITHDRAWAL_TOPIC = "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65";
|
|
4
4
|
export declare const DEPOSIT_TOPIC = "0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c";
|
|
5
5
|
/**
|
|
6
6
|
* Parse raw EVM logs into individual Transfer events.
|
|
7
7
|
*
|
|
8
|
+
* Iterates over per-tx `calls` and stamps each emitted `Transfer.txIdx`
|
|
9
|
+
* with the index of the originating call.
|
|
10
|
+
*
|
|
8
11
|
* **Supported event types:** ERC20 `Transfer(from, to, amount)` and WETH9
|
|
9
12
|
* `Deposit(to, amount)` / `Withdrawal(from, amount)`. WETH9 mint/burn Transfer
|
|
10
13
|
* events paired with their Deposit/Withdrawal are deduplicated. ERC721 and
|
|
11
14
|
* ERC1155 transfer events are **not** parsed — consumers with NFT flows will
|
|
12
15
|
* see an incomplete transfer list.
|
|
13
16
|
*
|
|
14
|
-
* **WETH9
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
17
|
+
* **WETH9 dedup assumption — canonical atomic emission.** Dedup is scoped to
|
|
18
|
+
* the same tx: a zero-address `Transfer` is suppressed only if its paired
|
|
19
|
+
* `Deposit`/`Withdrawal` appears in the same `calls[txIdx].logs` slice. This
|
|
20
|
+
* is correct for canonical WETH9, which always emits the `Deposit`/
|
|
21
|
+
* `Withdrawal` and the matching `Transfer(0x0, …)` / `Transfer(…, 0x0)`
|
|
22
|
+
* atomically inside a single call frame. A **non-canonical wrapped-native**
|
|
23
|
+
* that splits these emissions across two txs in the same bundle would leave
|
|
24
|
+
* a phantom zero-address `Transfer` in the parsed output, which can be summed
|
|
25
|
+
* by `assertNoBundlerRetention` and produce a false `BlacklistViolationError`.
|
|
26
|
+
* When a zero-address `Transfer` misses same-tx dedup *and* its token has
|
|
27
|
+
* emitted a `Deposit`/`Withdrawal` somewhere else in the bundle (i.e. it
|
|
28
|
+
* looks wnative-shaped), the parser emits a `warn` so the assumption break
|
|
29
|
+
* is observable before it reaches retention. None of `morpho-sdk`'s currently
|
|
30
|
+
* supported chains require cross-tx dedup; the assumption is rechecked when
|
|
31
|
+
* onboarding a new chain (see `evm-simulation/CLAUDE.md`).
|
|
18
32
|
*
|
|
19
33
|
* **Failure mode:** on a per-log parse failure (malformed topic length,
|
|
20
34
|
* non-hex data), the log is skipped and a `warn` is emitted via the logger.
|
|
21
|
-
* The caller receives a possibly-incomplete transfer list. Upstream sources
|
|
22
|
-
* (e.g., Tenderly response schemas) should validate log shape before calling
|
|
23
|
-
* here to avoid silent drops.
|
|
24
35
|
*
|
|
25
36
|
* Output is sorted canonically by token, from, to, amount for determinism.
|
|
37
|
+
* `txIdx` is attached but does not influence sort order.
|
|
26
38
|
*/
|
|
27
|
-
export declare function parseTransfers(
|
|
39
|
+
export declare function parseTransfers(calls: readonly RawCall[], logger?: SimulationLogger): Transfer[];
|