@morpho-org/evm-simulation 2.0.2 → 4.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 +22 -30
- package/lib/cjs/errors.d.ts +0 -6
- package/lib/cjs/errors.js +1 -11
- package/lib/cjs/index.d.ts +2 -3
- package/lib/cjs/index.js +3 -6
- package/lib/cjs/simulate/asset-changes.d.ts +19 -0
- package/lib/cjs/simulate/asset-changes.js +47 -0
- package/lib/cjs/simulate/backends/eth-simulate-v1.d.ts +9 -0
- package/lib/cjs/simulate/backends/eth-simulate-v1.js +39 -1
- package/lib/cjs/simulate/backends/index.d.ts +1 -1
- package/lib/cjs/simulate/backends/index.js +3 -3
- package/lib/cjs/simulate/backends/tenderly-rpc.d.ts +23 -0
- package/lib/cjs/simulate/backends/tenderly-rpc.js +230 -0
- package/lib/cjs/simulate/parsing/transfers.js +3 -1
- package/lib/cjs/simulate/pipeline/execute-simulation.d.ts +6 -10
- package/lib/cjs/simulate/pipeline/execute-simulation.js +11 -18
- package/lib/cjs/simulate/pipeline/resolve-chain.d.ts +6 -5
- package/lib/cjs/simulate/pipeline/resolve-chain.js +10 -7
- package/lib/cjs/simulate/simulate.d.ts +20 -23
- package/lib/cjs/simulate/simulate.js +21 -20
- package/lib/cjs/types.d.ts +58 -39
- package/lib/esm/errors.d.ts +0 -6
- package/lib/esm/errors.js +0 -9
- package/lib/esm/index.d.ts +2 -3
- package/lib/esm/index.js +1 -2
- package/lib/esm/simulate/asset-changes.d.ts +19 -0
- package/lib/esm/simulate/asset-changes.js +44 -0
- package/lib/esm/simulate/backends/eth-simulate-v1.d.ts +9 -0
- package/lib/esm/simulate/backends/eth-simulate-v1.js +40 -2
- package/lib/esm/simulate/backends/index.d.ts +1 -1
- package/lib/esm/simulate/backends/index.js +1 -1
- package/lib/esm/simulate/backends/tenderly-rpc.d.ts +23 -0
- package/lib/esm/simulate/backends/tenderly-rpc.js +227 -0
- package/lib/esm/simulate/parsing/transfers.js +3 -1
- package/lib/esm/simulate/pipeline/execute-simulation.d.ts +6 -10
- package/lib/esm/simulate/pipeline/execute-simulation.js +12 -19
- package/lib/esm/simulate/pipeline/resolve-chain.d.ts +6 -5
- package/lib/esm/simulate/pipeline/resolve-chain.js +10 -7
- package/lib/esm/simulate/simulate.d.ts +20 -23
- package/lib/esm/simulate/simulate.js +21 -20
- package/lib/esm/types.d.ts +58 -39
- package/package.json +7 -5
- package/src/index.ts +3 -3
- package/lib/cjs/screen-addresses/index.d.ts +0 -1
- package/lib/cjs/screen-addresses/index.js +0 -5
- package/lib/cjs/screen-addresses/sanctioned-addresses.d.ts +0 -11
- package/lib/cjs/screen-addresses/sanctioned-addresses.js +0 -77
- package/lib/cjs/screen-addresses/screen-addresses.d.ts +0 -50
- package/lib/cjs/screen-addresses/screen-addresses.js +0 -171
- package/lib/cjs/simulate/backends/tenderly-rest.d.ts +0 -20
- package/lib/cjs/simulate/backends/tenderly-rest.js +0 -279
- package/lib/esm/screen-addresses/index.d.ts +0 -1
- package/lib/esm/screen-addresses/index.js +0 -1
- package/lib/esm/screen-addresses/sanctioned-addresses.d.ts +0 -11
- package/lib/esm/screen-addresses/sanctioned-addresses.js +0 -74
- package/lib/esm/screen-addresses/screen-addresses.d.ts +0 -50
- package/lib/esm/screen-addresses/screen-addresses.js +0 -168
- package/lib/esm/simulate/backends/tenderly-rest.d.ts +0 -20
- package/lib/esm/simulate/backends/tenderly-rest.js +0 -276
|
@@ -23,33 +23,27 @@ const FALLBACK_MIN_BUDGET_MS = 1500;
|
|
|
23
23
|
*
|
|
24
24
|
* Dispatches the transaction bundle to the available backend with a shared timeout
|
|
25
25
|
* budget:
|
|
26
|
-
* - If Tenderly is configured for the chain, it gets
|
|
27
|
-
* the timeout. On `ExternalServiceError`,
|
|
28
|
-
* `max(remaining, FALLBACK_MIN_BUDGET_MS)`
|
|
29
|
-
* window when Tenderly eats its full slice.
|
|
30
|
-
* revert) propagates immediately without
|
|
31
|
-
* bundle, not the backend.
|
|
26
|
+
* - If a Tenderly RPC URL is configured for the chain, it gets
|
|
27
|
+
* `TENDERLY_BUDGET_RATIO` (60%) of the timeout. On `ExternalServiceError`,
|
|
28
|
+
* falls back to `eth_simulateV1` with `max(remaining, FALLBACK_MIN_BUDGET_MS)`
|
|
29
|
+
* so the fallback still has a viable window when Tenderly eats its full slice.
|
|
30
|
+
* `SimulationRevertedError` (contract revert) propagates immediately without
|
|
31
|
+
* retry — a revert is a property of the bundle, not the backend.
|
|
32
32
|
* - If only `eth_simulateV1` is configured, it gets the full timeout.
|
|
33
|
-
*
|
|
34
|
-
* `shareable` is Tenderly-only: when true, the backend persists the simulation and
|
|
35
|
-
* returns a shareable URL. `eth_simulateV1` ignores it (no persistence concept).
|
|
36
33
|
*/
|
|
37
34
|
async function executeSimulation(params) {
|
|
38
|
-
const { config, chainId, transactions, blockNumber
|
|
35
|
+
const { config, chainId, transactions, blockNumber } = params;
|
|
39
36
|
const chain = (0, resolve_chain_js_1.resolveChain)(config, chainId);
|
|
40
37
|
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
41
38
|
const deadline = Date.now() + timeoutMs;
|
|
42
|
-
if (chain.
|
|
39
|
+
if (chain.tenderlyRpc) {
|
|
43
40
|
const tenderlyTimeout = Math.floor(timeoutMs * TENDERLY_BUDGET_RATIO);
|
|
44
41
|
try {
|
|
45
|
-
return await (0, index_js_1.
|
|
46
|
-
config:
|
|
47
|
-
chainId,
|
|
42
|
+
return await (0, index_js_1.simulateTenderlyRpc)({
|
|
43
|
+
config: chain.tenderlyRpc,
|
|
48
44
|
transactions,
|
|
49
45
|
blockNumber,
|
|
50
|
-
shareable,
|
|
51
46
|
signal: AbortSignal.timeout(tenderlyTimeout),
|
|
52
|
-
logger: config.logger,
|
|
53
47
|
});
|
|
54
48
|
}
|
|
55
49
|
catch (error) {
|
|
@@ -62,8 +56,6 @@ async function executeSimulation(params) {
|
|
|
62
56
|
});
|
|
63
57
|
if (!chain.simulateV1Url)
|
|
64
58
|
throw error;
|
|
65
|
-
// Give the fallback a reasonable minimum even if Tenderly consumed the
|
|
66
|
-
// whole budget — otherwise slow-Tenderly == total outage.
|
|
67
59
|
const fallbackBudget = Math.max(deadline - Date.now(), FALLBACK_MIN_BUDGET_MS);
|
|
68
60
|
return await (0, index_js_1.simulateV1)({
|
|
69
61
|
rpcUrl: chain.simulateV1Url,
|
|
@@ -74,6 +66,7 @@ async function executeSimulation(params) {
|
|
|
74
66
|
});
|
|
75
67
|
}
|
|
76
68
|
}
|
|
69
|
+
/* v8 ignore next: resolveChain rejects this state before executeSimulation reaches the fallback path. */
|
|
77
70
|
if (!chain.simulateV1Url) {
|
|
78
71
|
throw new errors_js_1.UnsupportedChainError(chainId);
|
|
79
72
|
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import type { SimulationConfig } from "../../types.js";
|
|
1
|
+
import type { SimulationConfig, TenderlyRpcConfig } from "../../types.js";
|
|
2
2
|
interface ChainCapability {
|
|
3
|
-
|
|
3
|
+
tenderlyRpc?: TenderlyRpcConfig;
|
|
4
4
|
simulateV1Url?: string;
|
|
5
5
|
}
|
|
6
6
|
/**
|
|
7
7
|
* Stage 2 of the simulate() pipeline.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
* `UnsupportedChainError` when
|
|
11
|
-
*
|
|
9
|
+
* Looks up the per-chain `ChainSimulationConfig` and exposes whichever
|
|
10
|
+
* backends were configured. Throws `UnsupportedChainError` when the chain is
|
|
11
|
+
* absent from the map or has neither backend wired (defensive — the type
|
|
12
|
+
* already enforces at least one at construction).
|
|
12
13
|
*/
|
|
13
14
|
export declare function resolveChain(config: SimulationConfig, chainId: number): ChainCapability;
|
|
14
15
|
export {};
|
|
@@ -5,15 +5,18 @@ const errors_js_1 = require("../../errors.js");
|
|
|
5
5
|
/**
|
|
6
6
|
* Stage 2 of the simulate() pipeline.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* `UnsupportedChainError` when
|
|
10
|
-
*
|
|
8
|
+
* Looks up the per-chain `ChainSimulationConfig` and exposes whichever
|
|
9
|
+
* backends were configured. Throws `UnsupportedChainError` when the chain is
|
|
10
|
+
* absent from the map or has neither backend wired (defensive — the type
|
|
11
|
+
* already enforces at least one at construction).
|
|
11
12
|
*/
|
|
12
13
|
function resolveChain(config, chainId) {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
if (!tenderlySupported && !simulateV1Url) {
|
|
14
|
+
const entry = config.chains.get(chainId);
|
|
15
|
+
if (!entry || (!entry.tenderlyRpc && !entry.simulateV1Url)) {
|
|
16
16
|
throw new errors_js_1.UnsupportedChainError(chainId);
|
|
17
17
|
}
|
|
18
|
-
return {
|
|
18
|
+
return {
|
|
19
|
+
tenderlyRpc: entry.tenderlyRpc,
|
|
20
|
+
simulateV1Url: entry.simulateV1Url,
|
|
21
|
+
};
|
|
19
22
|
}
|
|
@@ -3,20 +3,21 @@ import type { SimulateParams, SimulationConfig, SimulationResult } from "../type
|
|
|
3
3
|
* Simulate a bundle of EVM transactions.
|
|
4
4
|
*
|
|
5
5
|
* Validates input → resolves authorizations into prepended approve txs → runs the bundle
|
|
6
|
-
* through Tenderly
|
|
6
|
+
* through Tenderly RPC (primary) or `eth_simulateV1` (fallback) with a shared timeout
|
|
7
7
|
* budget → parses ERC20/WETH transfers from per-tx logs → asserts no funds are retained
|
|
8
8
|
* by `bundler3` → returns the full result set. The caller reads whichever fields they need:
|
|
9
9
|
*
|
|
10
|
-
* - `transfers`
|
|
11
|
-
* - `simulationTxs` + `transfers` → server-side verification before broadcast
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* - `transfers` → user-facing preview / server-side verification.
|
|
11
|
+
* - `simulationTxs` + `transfers` → server-side verification before broadcast.
|
|
12
|
+
* - `calls[i]` → per-tx raw backend output (`logs`, `status`, `returnData`, `gasUsed`).
|
|
13
|
+
* Aligned 1:1 with `simulationTxs[i]`.
|
|
14
|
+
* - `assetChanges` → net per-asset balance changes grouped by account (sender and
|
|
15
|
+
* counterparties) over the whole bundle, normalized to the same shape across backends.
|
|
15
16
|
* - `transfers[k].txIdx` → index into `simulationTxs` of the tx that emitted the
|
|
16
17
|
* underlying log; consumers map back via `simulationTxs[transfer.txIdx]`.
|
|
17
18
|
*
|
|
18
|
-
* @param config - Backend configuration:
|
|
19
|
-
* logger, and the overall timeout budget.
|
|
19
|
+
* @param config - Backend configuration: per-chain Tenderly RPC and/or `eth_simulateV1`
|
|
20
|
+
* URL, optional logger, and the overall timeout budget.
|
|
20
21
|
* @param params - Per-call simulation input.
|
|
21
22
|
* @param params.chainId - Chain id the bundle targets.
|
|
22
23
|
* @param params.transactions - The bundle's transactions, in execution order. All must share the
|
|
@@ -24,10 +25,6 @@ import type { SimulateParams, SimulationConfig, SimulationResult } from "../type
|
|
|
24
25
|
* @param params.authorizations - Optional token authorizations resolved into prepended approve
|
|
25
26
|
* transactions before the main bundle runs.
|
|
26
27
|
* @param params.blockNumber - Optional pinned block number or `BlockTag`. Defaults to `latest`.
|
|
27
|
-
* @param options - Per-call simulation options.
|
|
28
|
-
* @param options.shareable - When `true`, persists the simulation in Tenderly and returns a
|
|
29
|
-
* shareable `tenderlyUrl`. Honored only on the Tenderly backend, not on the `eth_simulateV1`
|
|
30
|
-
* fallback. Defaults to `false`.
|
|
31
28
|
* @throws {SimulationValidationError} for invalid input (mixed senders, bad addresses,
|
|
32
29
|
* empty transactions, malformed authorizations).
|
|
33
30
|
* @throws {UnsupportedChainError} when the chain is not configured for any backend.
|
|
@@ -39,14 +36,21 @@ import type { SimulateParams, SimulationConfig, SimulationResult } from "../type
|
|
|
39
36
|
* not match the resolved `simulationTxs` — refusing to map transfers with mismatched
|
|
40
37
|
* per-tx output.
|
|
41
38
|
* @returns A {@link SimulationResult} carrying the resolved `simulationTxs`, per-tx
|
|
42
|
-
* `calls` (aligned 1:1 with `simulationTxs`), parsed `transfers` (each stamped
|
|
43
|
-
* `txIdx`), and
|
|
39
|
+
* `calls` (aligned 1:1 with `simulationTxs`), parsed `transfers` (each stamped
|
|
40
|
+
* with `txIdx`), and per-account net `assetChanges`.
|
|
44
41
|
* @example
|
|
45
42
|
* ```ts
|
|
46
43
|
* import { simulate } from "@morpho-org/evm-simulation";
|
|
47
44
|
*
|
|
48
45
|
* const result = await simulate(
|
|
49
|
-
* {
|
|
46
|
+
* {
|
|
47
|
+
* chains: new Map([
|
|
48
|
+
* [1, {
|
|
49
|
+
* tenderlyRpc: { rpcUrl: process.env.TENDERLY_RPC_URL! },
|
|
50
|
+
* simulateV1Url: process.env.MAINNET_RPC_URL,
|
|
51
|
+
* }],
|
|
52
|
+
* ]),
|
|
53
|
+
* },
|
|
50
54
|
* {
|
|
51
55
|
* chainId: 1,
|
|
52
56
|
* transactions: [{ from: user, to: vaultAddress, data: encodedCalldata, value: 0n }],
|
|
@@ -55,11 +59,4 @@ import type { SimulateParams, SimulationConfig, SimulationResult } from "../type
|
|
|
55
59
|
* // result satisfies SimulationResult
|
|
56
60
|
* ```
|
|
57
61
|
*/
|
|
58
|
-
export declare function simulate(config: SimulationConfig, params: SimulateParams
|
|
59
|
-
/**
|
|
60
|
-
* When true, persist the simulation in Tenderly and return a shareable
|
|
61
|
-
* `tenderlyUrl` in the result. Only honored when the Tenderly backend runs
|
|
62
|
-
* (not the `eth_simulateV1` fallback). Defaults to `false`.
|
|
63
|
-
*/
|
|
64
|
-
shareable?: boolean;
|
|
65
|
-
}): Promise<SimulationResult>;
|
|
62
|
+
export declare function simulate(config: SimulationConfig, params: SimulateParams): Promise<SimulationResult>;
|
|
@@ -8,20 +8,21 @@ const index_js_2 = require("./pipeline/index.js");
|
|
|
8
8
|
* Simulate a bundle of EVM transactions.
|
|
9
9
|
*
|
|
10
10
|
* Validates input → resolves authorizations into prepended approve txs → runs the bundle
|
|
11
|
-
* through Tenderly
|
|
11
|
+
* through Tenderly RPC (primary) or `eth_simulateV1` (fallback) with a shared timeout
|
|
12
12
|
* budget → parses ERC20/WETH transfers from per-tx logs → asserts no funds are retained
|
|
13
13
|
* by `bundler3` → returns the full result set. The caller reads whichever fields they need:
|
|
14
14
|
*
|
|
15
|
-
* - `transfers`
|
|
16
|
-
* - `simulationTxs` + `transfers` → server-side verification before broadcast
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
15
|
+
* - `transfers` → user-facing preview / server-side verification.
|
|
16
|
+
* - `simulationTxs` + `transfers` → server-side verification before broadcast.
|
|
17
|
+
* - `calls[i]` → per-tx raw backend output (`logs`, `status`, `returnData`, `gasUsed`).
|
|
18
|
+
* Aligned 1:1 with `simulationTxs[i]`.
|
|
19
|
+
* - `assetChanges` → net per-asset balance changes grouped by account (sender and
|
|
20
|
+
* counterparties) over the whole bundle, normalized to the same shape across backends.
|
|
20
21
|
* - `transfers[k].txIdx` → index into `simulationTxs` of the tx that emitted the
|
|
21
22
|
* underlying log; consumers map back via `simulationTxs[transfer.txIdx]`.
|
|
22
23
|
*
|
|
23
|
-
* @param config - Backend configuration:
|
|
24
|
-
* logger, and the overall timeout budget.
|
|
24
|
+
* @param config - Backend configuration: per-chain Tenderly RPC and/or `eth_simulateV1`
|
|
25
|
+
* URL, optional logger, and the overall timeout budget.
|
|
25
26
|
* @param params - Per-call simulation input.
|
|
26
27
|
* @param params.chainId - Chain id the bundle targets.
|
|
27
28
|
* @param params.transactions - The bundle's transactions, in execution order. All must share the
|
|
@@ -29,10 +30,6 @@ const index_js_2 = require("./pipeline/index.js");
|
|
|
29
30
|
* @param params.authorizations - Optional token authorizations resolved into prepended approve
|
|
30
31
|
* transactions before the main bundle runs.
|
|
31
32
|
* @param params.blockNumber - Optional pinned block number or `BlockTag`. Defaults to `latest`.
|
|
32
|
-
* @param options - Per-call simulation options.
|
|
33
|
-
* @param options.shareable - When `true`, persists the simulation in Tenderly and returns a
|
|
34
|
-
* shareable `tenderlyUrl`. Honored only on the Tenderly backend, not on the `eth_simulateV1`
|
|
35
|
-
* fallback. Defaults to `false`.
|
|
36
33
|
* @throws {SimulationValidationError} for invalid input (mixed senders, bad addresses,
|
|
37
34
|
* empty transactions, malformed authorizations).
|
|
38
35
|
* @throws {UnsupportedChainError} when the chain is not configured for any backend.
|
|
@@ -44,14 +41,21 @@ const index_js_2 = require("./pipeline/index.js");
|
|
|
44
41
|
* not match the resolved `simulationTxs` — refusing to map transfers with mismatched
|
|
45
42
|
* per-tx output.
|
|
46
43
|
* @returns A {@link SimulationResult} carrying the resolved `simulationTxs`, per-tx
|
|
47
|
-
* `calls` (aligned 1:1 with `simulationTxs`), parsed `transfers` (each stamped
|
|
48
|
-
* `txIdx`), and
|
|
44
|
+
* `calls` (aligned 1:1 with `simulationTxs`), parsed `transfers` (each stamped
|
|
45
|
+
* with `txIdx`), and per-account net `assetChanges`.
|
|
49
46
|
* @example
|
|
50
47
|
* ```ts
|
|
51
48
|
* import { simulate } from "@morpho-org/evm-simulation";
|
|
52
49
|
*
|
|
53
50
|
* const result = await simulate(
|
|
54
|
-
* {
|
|
51
|
+
* {
|
|
52
|
+
* chains: new Map([
|
|
53
|
+
* [1, {
|
|
54
|
+
* tenderlyRpc: { rpcUrl: process.env.TENDERLY_RPC_URL! },
|
|
55
|
+
* simulateV1Url: process.env.MAINNET_RPC_URL,
|
|
56
|
+
* }],
|
|
57
|
+
* ]),
|
|
58
|
+
* },
|
|
55
59
|
* {
|
|
56
60
|
* chainId: 1,
|
|
57
61
|
* transactions: [{ from: user, to: vaultAddress, data: encodedCalldata, value: 0n }],
|
|
@@ -60,9 +64,7 @@ const index_js_2 = require("./pipeline/index.js");
|
|
|
60
64
|
* // result satisfies SimulationResult
|
|
61
65
|
* ```
|
|
62
66
|
*/
|
|
63
|
-
|
|
64
|
-
async function simulate(config, params, options = {}) {
|
|
65
|
-
const { shareable = false } = options;
|
|
67
|
+
async function simulate(config, params) {
|
|
66
68
|
(0, index_js_2.validateInput)(params);
|
|
67
69
|
const simulationTxs = (0, index_js_2.buildSimulationTxs)(params);
|
|
68
70
|
const result = await (0, index_js_2.executeSimulation)({
|
|
@@ -70,7 +72,6 @@ async function simulate(config, params, options = {}) {
|
|
|
70
72
|
chainId: params.chainId,
|
|
71
73
|
transactions: simulationTxs,
|
|
72
74
|
blockNumber: params.blockNumber,
|
|
73
|
-
shareable,
|
|
74
75
|
});
|
|
75
76
|
if (result.calls.length !== simulationTxs.length) {
|
|
76
77
|
throw new errors_js_1.ExternalServiceError(`Backend returned ${result.calls.length} call result(s) for ${simulationTxs.length} transaction(s) — refusing to map transfers with mismatched lengths`);
|
|
@@ -85,6 +86,6 @@ async function simulate(config, params, options = {}) {
|
|
|
85
86
|
simulationTxs,
|
|
86
87
|
calls: result.calls,
|
|
87
88
|
transfers,
|
|
88
|
-
|
|
89
|
+
assetChanges: result.assetChanges,
|
|
89
90
|
};
|
|
90
91
|
}
|
package/lib/cjs/types.d.ts
CHANGED
|
@@ -1,36 +1,34 @@
|
|
|
1
1
|
import type { Address, BlockTag, Hex } from "viem";
|
|
2
2
|
/**
|
|
3
|
-
* Credentials
|
|
4
|
-
*
|
|
3
|
+
* Credentials for the Tenderly Node Web3 Gateway. The `rpcUrl` is the
|
|
4
|
+
* chain-specific gateway URL with the access key embedded in the path,
|
|
5
|
+
* e.g. `https://mainnet.gateway.tenderly.co/{ACCESS_KEY}`.
|
|
5
6
|
*/
|
|
6
|
-
export interface
|
|
7
|
-
|
|
8
|
-
apiBaseUrl: string;
|
|
9
|
-
accessToken: string;
|
|
10
|
-
accountSlug: string;
|
|
11
|
-
projectSlug: string;
|
|
12
|
-
/** Chain IDs that Tenderly supports for simulation */
|
|
13
|
-
supportedChainIds: Set<number>;
|
|
7
|
+
export interface TenderlyRpcConfig {
|
|
8
|
+
rpcUrl: string;
|
|
14
9
|
}
|
|
15
10
|
/**
|
|
16
|
-
* Per-chain capabilities.
|
|
17
|
-
*
|
|
11
|
+
* Per-chain backend capabilities. Exactly one configuration shape per chain;
|
|
12
|
+
* the discriminated union enforces that **at least one** of `tenderlyRpc`
|
|
13
|
+
* (primary) or `simulateV1Url` (fallback) is supplied.
|
|
18
14
|
*/
|
|
19
|
-
export
|
|
20
|
-
/**
|
|
15
|
+
export type ChainSimulationConfig = {
|
|
16
|
+
/** Tenderly RPC config — `tenderly_simulateTransaction` / `tenderly_simulateBundle`. */
|
|
17
|
+
tenderlyRpc: TenderlyRpcConfig;
|
|
18
|
+
/** JSON-RPC URL supporting `eth_simulateV1`. Optional when Tenderly is set. */
|
|
21
19
|
simulateV1Url?: string;
|
|
22
|
-
}
|
|
20
|
+
} | {
|
|
21
|
+
tenderlyRpc?: TenderlyRpcConfig;
|
|
22
|
+
simulateV1Url: string;
|
|
23
|
+
};
|
|
23
24
|
/**
|
|
24
25
|
* Top-level configuration for `simulate`.
|
|
25
26
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* orchestrator throws `UnsupportedChainError`.
|
|
27
|
+
* Every chain entry must declare at least one backend (Tenderly RPC primary,
|
|
28
|
+
* `eth_simulateV1` fallback, or both). Calling `simulate` for a `chainId`
|
|
29
|
+
* missing from `chains` throws `UnsupportedChainError`.
|
|
30
30
|
*/
|
|
31
31
|
export interface SimulationConfig {
|
|
32
|
-
/** Tenderly REST API config. undefined = Tenderly not available. */
|
|
33
|
-
tenderlyRest?: TenderlyRestConfig;
|
|
34
32
|
/** Per-chain simulation capabilities. */
|
|
35
33
|
chains: Map<number, ChainSimulationConfig>;
|
|
36
34
|
logger?: SimulationLogger;
|
|
@@ -66,6 +64,33 @@ export type SimulationAuthorization = {
|
|
|
66
64
|
/** Defaults to maxUint256. Caller can set explicit amount for tokens that don't support max approval. */
|
|
67
65
|
amount?: bigint;
|
|
68
66
|
};
|
|
67
|
+
/**
|
|
68
|
+
* Net balance change for a single asset (one token) within an account's entry.
|
|
69
|
+
* Native ETH uses viem's `ethAddress` sentinel as `token`. `symbol`/`decimals`
|
|
70
|
+
* are best-effort and may be absent, notably on the `eth_simulateV1` fallback.
|
|
71
|
+
*/
|
|
72
|
+
export interface AssetChange {
|
|
73
|
+
readonly token: Address;
|
|
74
|
+
readonly symbol?: string;
|
|
75
|
+
readonly decimals?: number;
|
|
76
|
+
/** Signed net change of the account's balance, in raw token units. */
|
|
77
|
+
readonly diff: bigint;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Net per-token balance changes for one account over the whole bundle. Returned
|
|
81
|
+
* for every account that nets a non-zero change, the sender and counterparties
|
|
82
|
+
* alike (the zero address is kept for mints/burns). Accounts and their `changes`
|
|
83
|
+
* are sorted by address for deterministic, cross-backend output.
|
|
84
|
+
*
|
|
85
|
+
* Native-ETH coverage differs by backend: Tenderly reports the full net ETH
|
|
86
|
+
* delta (including ETH moved via internal calls), while the `eth_simulateV1`
|
|
87
|
+
* fallback derives native ETH only from the top-level transaction `value`s —
|
|
88
|
+
* ETH moved internally (e.g. a `WETH.withdraw` refund) is not captured there.
|
|
89
|
+
*/
|
|
90
|
+
export interface AccountAssetChanges {
|
|
91
|
+
readonly account: Address;
|
|
92
|
+
readonly changes: readonly AssetChange[];
|
|
93
|
+
}
|
|
69
94
|
/**
|
|
70
95
|
* A parsed ERC20 / WETH9 transfer extracted from simulation logs. Returned in
|
|
71
96
|
* `SimulationResult.transfers`.
|
|
@@ -89,25 +114,25 @@ export interface Transfer {
|
|
|
89
114
|
* - `simulationTxs` is the full resolved transaction list (including
|
|
90
115
|
* prepended authorization txs).
|
|
91
116
|
* - `calls[i]` corresponds 1:1 with `simulationTxs[i]` — read raw logs,
|
|
92
|
-
* status, returnData/gasUsed
|
|
117
|
+
* status, returnData/gasUsed.
|
|
118
|
+
* - `assetChanges` is the net per-asset balance change over the whole bundle,
|
|
119
|
+
* grouped by account (sender and counterparties), normalized to the same
|
|
120
|
+
* shape across backends — see `AccountAssetChanges`.
|
|
93
121
|
* - `transfers[k].txIdx` indexes into `simulationTxs` to attribute each
|
|
94
122
|
* transfer to its emitting transaction.
|
|
95
|
-
* - `tenderlyUrl` is set only when `shareable: true` AND the Tenderly backend
|
|
96
|
-
* ran successfully (not the `eth_simulateV1` fallback).
|
|
97
123
|
*/
|
|
98
124
|
export interface SimulationResult {
|
|
99
125
|
/** The full resolved transaction list (including prepended authorization txs). */
|
|
100
126
|
readonly simulationTxs: readonly SimulationTransaction[];
|
|
101
127
|
/**
|
|
102
128
|
* Per-transaction normalized output. `calls[i]` corresponds 1:1 with
|
|
103
|
-
* `simulationTxs[i]`. Use this to read raw logs, status, return data, gas
|
|
104
|
-
* used, and (Tenderly only) asset changes per transaction.
|
|
129
|
+
* `simulationTxs[i]`. Use this to read raw logs, status, return data, gas used.
|
|
105
130
|
*/
|
|
106
131
|
readonly calls: readonly SimulationCall[];
|
|
107
132
|
/** Parsed ERC-20 / WETH9 transfers from the simulation. */
|
|
108
133
|
readonly transfers: readonly Transfer[];
|
|
109
|
-
/**
|
|
110
|
-
readonly
|
|
134
|
+
/** Net per-asset balance changes, grouped by account, over the whole bundle. */
|
|
135
|
+
readonly assetChanges: readonly AccountAssetChanges[];
|
|
111
136
|
}
|
|
112
137
|
/** Minimal structured logger the package calls for warnings and info. */
|
|
113
138
|
export interface SimulationLogger {
|
|
@@ -128,15 +153,15 @@ export interface SimulateParams {
|
|
|
128
153
|
/**
|
|
129
154
|
* Internal raw result from a simulation adapter before normalization.
|
|
130
155
|
* `calls[i]` corresponds 1:1 with the i-th transaction passed to the
|
|
131
|
-
* backend.
|
|
156
|
+
* backend; `assetChanges` is the bundle-level aggregate grouped by account.
|
|
132
157
|
*/
|
|
133
158
|
export interface RawSimulationResult {
|
|
134
159
|
calls: RawCall[];
|
|
135
|
-
|
|
160
|
+
assetChanges: AccountAssetChanges[];
|
|
136
161
|
}
|
|
137
162
|
/**
|
|
138
163
|
* Normalized EVM log emitted by a single simulated call. The shape is the
|
|
139
|
-
* common subset both backends (`eth_simulateV1` via viem and Tenderly
|
|
164
|
+
* common subset both backends (`eth_simulateV1` via viem and Tenderly RPC)
|
|
140
165
|
* produce after schema validation. Returned indirectly via
|
|
141
166
|
* `SimulationCall.logs` and consumed by the SDK's transfer parser.
|
|
142
167
|
*/
|
|
@@ -156,15 +181,14 @@ export interface RawCall {
|
|
|
156
181
|
status: boolean;
|
|
157
182
|
returnData: Hex;
|
|
158
183
|
gasUsed: bigint;
|
|
159
|
-
/** Tenderly-only `asset_changes` payload. Absent on `eth_simulateV1`. */
|
|
160
|
-
assetChanges?: unknown;
|
|
161
184
|
}
|
|
162
185
|
/**
|
|
163
186
|
* Per-transaction normalized output from the simulation backend.
|
|
164
187
|
*
|
|
165
188
|
* `SimulationResult.calls[i]` corresponds 1:1 with
|
|
166
189
|
* `SimulationResult.simulationTxs[i]`. Use this to read raw logs, status,
|
|
167
|
-
* return data, gas used
|
|
190
|
+
* return data, and gas used. Net asset changes are reported at the bundle
|
|
191
|
+
* level — see `SimulationResult.assetChanges`.
|
|
168
192
|
*/
|
|
169
193
|
export interface SimulationCall {
|
|
170
194
|
readonly logs: readonly RawLog[];
|
|
@@ -179,9 +203,4 @@ export interface SimulationCall {
|
|
|
179
203
|
readonly returnData: Hex;
|
|
180
204
|
/** Gas used by this call (root frame). */
|
|
181
205
|
readonly gasUsed: bigint;
|
|
182
|
-
/**
|
|
183
|
-
* Tenderly-only `asset_changes` payload for this tx. Opaque `unknown` —
|
|
184
|
-
* do not destructure without validation. Absent on `eth_simulateV1`.
|
|
185
|
-
*/
|
|
186
|
-
readonly assetChanges?: unknown;
|
|
187
206
|
}
|
package/lib/esm/errors.d.ts
CHANGED
|
@@ -25,12 +25,6 @@ export declare class BlacklistViolationError extends SimulationPackageError {
|
|
|
25
25
|
readonly code = "BLACKLIST_ERROR";
|
|
26
26
|
constructor(message: string, assetChanges?: RetainedAsset[] | undefined);
|
|
27
27
|
}
|
|
28
|
-
/** Sanctioned or compromised address detected. Never bypassable. */
|
|
29
|
-
export declare class AddressScreeningError extends SimulationPackageError {
|
|
30
|
-
readonly addresses: string[];
|
|
31
|
-
readonly code = "SCREENING_ERROR";
|
|
32
|
-
constructor(message: string, addresses: string[]);
|
|
33
|
-
}
|
|
34
28
|
/** Tenderly or RPC service is down. Bypassable — user can proceed. */
|
|
35
29
|
export declare class ExternalServiceError extends SimulationPackageError {
|
|
36
30
|
readonly code = "EXTERNAL_SERVICE_ERROR";
|
package/lib/esm/errors.js
CHANGED
|
@@ -28,15 +28,6 @@ export class BlacklistViolationError extends SimulationPackageError {
|
|
|
28
28
|
this.assetChanges = assetChanges;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
-
/** Sanctioned or compromised address detected. Never bypassable. */
|
|
32
|
-
export class AddressScreeningError extends SimulationPackageError {
|
|
33
|
-
addresses;
|
|
34
|
-
code = "SCREENING_ERROR";
|
|
35
|
-
constructor(message, addresses) {
|
|
36
|
-
super(message);
|
|
37
|
-
this.addresses = addresses;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
31
|
/** Tenderly or RPC service is down. Bypassable — user can proceed. */
|
|
41
32
|
export class ExternalServiceError extends SimulationPackageError {
|
|
42
33
|
code = "EXTERNAL_SERVICE_ERROR";
|
package/lib/esm/index.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export { screenAddresses } from "./screen-addresses/index.js";
|
|
1
|
+
export { BlacklistViolationError, ExternalServiceError, SimulationPackageError, SimulationRevertedError, SimulationValidationError, UnsupportedChainError, } from "./errors.js";
|
|
3
2
|
export { simulate } from "./simulate/index.js";
|
|
4
|
-
export type { ChainSimulationConfig, RawLog, SimulateParams, SimulationAuthorization, SimulationCall, SimulationConfig, SimulationLogger, SimulationResult, SimulationTransaction,
|
|
3
|
+
export type { AccountAssetChanges, AssetChange, ChainSimulationConfig, RawLog, SimulateParams, SimulationAuthorization, SimulationCall, SimulationConfig, SimulationLogger, SimulationResult, SimulationTransaction, TenderlyRpcConfig, Transfer, } from "./types.js";
|
package/lib/esm/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// Public fns (via feature-folder barrels)
|
|
2
2
|
// Errors (for instanceof checks by consumers)
|
|
3
|
-
export {
|
|
4
|
-
export { screenAddresses } from "./screen-addresses/index.js";
|
|
3
|
+
export { BlacklistViolationError, ExternalServiceError, SimulationPackageError, SimulationRevertedError, SimulationValidationError, UnsupportedChainError, } from "./errors.js";
|
|
5
4
|
export { simulate } from "./simulate/index.js";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type Address } from "viem";
|
|
2
|
+
import type { AccountAssetChanges } from "../types.js";
|
|
3
|
+
/** A single signed contribution to one account's balance for one token. */
|
|
4
|
+
export interface AssetChangeEntry {
|
|
5
|
+
account: Address;
|
|
6
|
+
token: Address;
|
|
7
|
+
diff: bigint;
|
|
8
|
+
symbol?: string;
|
|
9
|
+
decimals?: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Net signed per-(account, token) contributions into balance changes grouped by
|
|
13
|
+
* account. Accounts and their `changes` are sorted by address for deterministic
|
|
14
|
+
* output; zero-net tokens and accounts are dropped.
|
|
15
|
+
*
|
|
16
|
+
* @param entries - Signed contributions; `diff` is added per (account, token).
|
|
17
|
+
* @returns One {@link AccountAssetChanges} per account with a non-zero change.
|
|
18
|
+
*/
|
|
19
|
+
export declare function groupAssetChanges(entries: AssetChangeEntry[]): AccountAssetChanges[];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { getAddress } from "viem";
|
|
2
|
+
// Locale-independent byte-order compare on lowercased hex, matching `sortTransfers`.
|
|
3
|
+
const compareAddress = (a, b) => {
|
|
4
|
+
const al = a.toLowerCase();
|
|
5
|
+
const bl = b.toLowerCase();
|
|
6
|
+
return al < bl ? -1 : al > bl ? 1 : 0;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Net signed per-(account, token) contributions into balance changes grouped by
|
|
10
|
+
* account. Accounts and their `changes` are sorted by address for deterministic
|
|
11
|
+
* output; zero-net tokens and accounts are dropped.
|
|
12
|
+
*
|
|
13
|
+
* @param entries - Signed contributions; `diff` is added per (account, token).
|
|
14
|
+
* @returns One {@link AccountAssetChanges} per account with a non-zero change.
|
|
15
|
+
*/
|
|
16
|
+
export function groupAssetChanges(entries) {
|
|
17
|
+
const byAccount = new Map();
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
if (entry.diff === 0n)
|
|
20
|
+
continue;
|
|
21
|
+
const account = getAddress(entry.account);
|
|
22
|
+
// `token` is normalized by callers (checksummed contract or `ethAddress`).
|
|
23
|
+
const token = entry.token;
|
|
24
|
+
let tokens = byAccount.get(account);
|
|
25
|
+
if (!tokens)
|
|
26
|
+
byAccount.set(account, (tokens = new Map()));
|
|
27
|
+
const prev = tokens.get(token);
|
|
28
|
+
tokens.set(token, {
|
|
29
|
+
token,
|
|
30
|
+
symbol: prev?.symbol ?? entry.symbol,
|
|
31
|
+
decimals: prev?.decimals ?? entry.decimals,
|
|
32
|
+
diff: (prev?.diff ?? 0n) + entry.diff,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return [...byAccount]
|
|
36
|
+
.map(([account, tokens]) => ({
|
|
37
|
+
account,
|
|
38
|
+
changes: [...tokens.values()]
|
|
39
|
+
.filter((c) => c.diff !== 0n)
|
|
40
|
+
.sort((x, y) => compareAddress(x.token, y.token)),
|
|
41
|
+
}))
|
|
42
|
+
.filter((a) => a.changes.length > 0)
|
|
43
|
+
.sort((a, b) => compareAddress(a.account, b.account));
|
|
44
|
+
}
|
|
@@ -5,6 +5,15 @@ import type { RawSimulationResult, SimulationTransaction } from "../../types.js"
|
|
|
5
5
|
*
|
|
6
6
|
* Uses stateOverride to set the user's ETH balance high enough to avoid
|
|
7
7
|
* false "insufficient balance for gas" reverts.
|
|
8
|
+
*
|
|
9
|
+
* Derives `assetChanges` (net per-token deltas grouped by account) from the
|
|
10
|
+
* emitted transfer logs, plus native ETH from each transaction's top-level
|
|
11
|
+
* `value` (payer debited, recipient credited, under viem's `ethAddress`).
|
|
12
|
+
*
|
|
13
|
+
* Coverage on this path is therefore narrower than the Tenderly backend:
|
|
14
|
+
* native ETH moved through internal calls (e.g. a `WETH.withdraw` refund, a
|
|
15
|
+
* swap that pays out ETH) emits no log and is not reflected in `value`, so it
|
|
16
|
+
* is not captured. For full native-ETH accounting use Tenderly.
|
|
8
17
|
*/
|
|
9
18
|
export declare function simulateV1(params: {
|
|
10
19
|
rpcUrl: string;
|
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
import { createPublicClient, getAddress, http, maxUint256, } from "viem";
|
|
1
|
+
import { createPublicClient, ethAddress, getAddress, http, maxUint256, } from "viem";
|
|
2
2
|
import { ExternalServiceError, SimulationRevertedError, SimulationValidationError, } from "../../errors.js";
|
|
3
|
+
import { groupAssetChanges } from "../asset-changes.js";
|
|
4
|
+
import { parseTransfers } from "../parsing/index.js";
|
|
3
5
|
/**
|
|
4
6
|
* Simulate transactions using eth_simulateV1 (viem simulateCalls).
|
|
5
7
|
*
|
|
6
8
|
* Uses stateOverride to set the user's ETH balance high enough to avoid
|
|
7
9
|
* false "insufficient balance for gas" reverts.
|
|
10
|
+
*
|
|
11
|
+
* Derives `assetChanges` (net per-token deltas grouped by account) from the
|
|
12
|
+
* emitted transfer logs, plus native ETH from each transaction's top-level
|
|
13
|
+
* `value` (payer debited, recipient credited, under viem's `ethAddress`).
|
|
14
|
+
*
|
|
15
|
+
* Coverage on this path is therefore narrower than the Tenderly backend:
|
|
16
|
+
* native ETH moved through internal calls (e.g. a `WETH.withdraw` refund, a
|
|
17
|
+
* swap that pays out ETH) emits no log and is not reflected in `value`, so it
|
|
18
|
+
* is not captured. For full native-ETH accounting use Tenderly.
|
|
8
19
|
*/
|
|
9
20
|
export async function simulateV1(params) {
|
|
10
21
|
const { rpcUrl, transactions, blockNumber, signal } = params;
|
|
@@ -61,7 +72,13 @@ export async function simulateV1(params) {
|
|
|
61
72
|
returnData: r.data ?? "0x",
|
|
62
73
|
gasUsed: r.gasUsed,
|
|
63
74
|
}));
|
|
64
|
-
return {
|
|
75
|
+
return {
|
|
76
|
+
calls: rawCalls,
|
|
77
|
+
assetChanges: toAssetChanges({
|
|
78
|
+
transfers: parseTransfers(rawCalls),
|
|
79
|
+
transactions,
|
|
80
|
+
}),
|
|
81
|
+
};
|
|
65
82
|
}
|
|
66
83
|
catch (error) {
|
|
67
84
|
if (error instanceof SimulationRevertedError)
|
|
@@ -71,3 +88,24 @@ export async function simulateV1(params) {
|
|
|
71
88
|
throw new ExternalServiceError(`eth_simulateV1 error: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
72
89
|
}
|
|
73
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Reduce parsed transfer logs and top-level transaction values to net per-token
|
|
93
|
+
* balance changes grouped by account. Native ETH is taken from the top-level
|
|
94
|
+
* `value`s (payer debited, recipient credited — internal moves are not logged,
|
|
95
|
+
* see `simulateV1` for the coverage caveat).
|
|
96
|
+
*/
|
|
97
|
+
function toAssetChanges(params) {
|
|
98
|
+
const { transfers, transactions } = params;
|
|
99
|
+
const entries = [];
|
|
100
|
+
for (const tx of transactions) {
|
|
101
|
+
if (!tx.value)
|
|
102
|
+
continue;
|
|
103
|
+
entries.push({ account: tx.from, token: ethAddress, diff: -tx.value });
|
|
104
|
+
entries.push({ account: tx.to, token: ethAddress, diff: tx.value });
|
|
105
|
+
}
|
|
106
|
+
for (const { token, from, to, amount } of transfers) {
|
|
107
|
+
entries.push({ account: to, token, diff: amount });
|
|
108
|
+
entries.push({ account: from, token, diff: -amount });
|
|
109
|
+
}
|
|
110
|
+
return groupAssetChanges(entries);
|
|
111
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { simulateV1 } from "./eth-simulate-v1.js";
|
|
2
|
-
export {
|
|
2
|
+
export { simulateTenderlyRpc } from "./tenderly-rpc.js";
|