@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.
Files changed (59) hide show
  1. package/README.md +22 -30
  2. package/lib/cjs/errors.d.ts +0 -6
  3. package/lib/cjs/errors.js +1 -11
  4. package/lib/cjs/index.d.ts +2 -3
  5. package/lib/cjs/index.js +3 -6
  6. package/lib/cjs/simulate/asset-changes.d.ts +19 -0
  7. package/lib/cjs/simulate/asset-changes.js +47 -0
  8. package/lib/cjs/simulate/backends/eth-simulate-v1.d.ts +9 -0
  9. package/lib/cjs/simulate/backends/eth-simulate-v1.js +39 -1
  10. package/lib/cjs/simulate/backends/index.d.ts +1 -1
  11. package/lib/cjs/simulate/backends/index.js +3 -3
  12. package/lib/cjs/simulate/backends/tenderly-rpc.d.ts +23 -0
  13. package/lib/cjs/simulate/backends/tenderly-rpc.js +230 -0
  14. package/lib/cjs/simulate/parsing/transfers.js +3 -1
  15. package/lib/cjs/simulate/pipeline/execute-simulation.d.ts +6 -10
  16. package/lib/cjs/simulate/pipeline/execute-simulation.js +11 -18
  17. package/lib/cjs/simulate/pipeline/resolve-chain.d.ts +6 -5
  18. package/lib/cjs/simulate/pipeline/resolve-chain.js +10 -7
  19. package/lib/cjs/simulate/simulate.d.ts +20 -23
  20. package/lib/cjs/simulate/simulate.js +21 -20
  21. package/lib/cjs/types.d.ts +58 -39
  22. package/lib/esm/errors.d.ts +0 -6
  23. package/lib/esm/errors.js +0 -9
  24. package/lib/esm/index.d.ts +2 -3
  25. package/lib/esm/index.js +1 -2
  26. package/lib/esm/simulate/asset-changes.d.ts +19 -0
  27. package/lib/esm/simulate/asset-changes.js +44 -0
  28. package/lib/esm/simulate/backends/eth-simulate-v1.d.ts +9 -0
  29. package/lib/esm/simulate/backends/eth-simulate-v1.js +40 -2
  30. package/lib/esm/simulate/backends/index.d.ts +1 -1
  31. package/lib/esm/simulate/backends/index.js +1 -1
  32. package/lib/esm/simulate/backends/tenderly-rpc.d.ts +23 -0
  33. package/lib/esm/simulate/backends/tenderly-rpc.js +227 -0
  34. package/lib/esm/simulate/parsing/transfers.js +3 -1
  35. package/lib/esm/simulate/pipeline/execute-simulation.d.ts +6 -10
  36. package/lib/esm/simulate/pipeline/execute-simulation.js +12 -19
  37. package/lib/esm/simulate/pipeline/resolve-chain.d.ts +6 -5
  38. package/lib/esm/simulate/pipeline/resolve-chain.js +10 -7
  39. package/lib/esm/simulate/simulate.d.ts +20 -23
  40. package/lib/esm/simulate/simulate.js +21 -20
  41. package/lib/esm/types.d.ts +58 -39
  42. package/package.json +7 -5
  43. package/src/index.ts +3 -3
  44. package/lib/cjs/screen-addresses/index.d.ts +0 -1
  45. package/lib/cjs/screen-addresses/index.js +0 -5
  46. package/lib/cjs/screen-addresses/sanctioned-addresses.d.ts +0 -11
  47. package/lib/cjs/screen-addresses/sanctioned-addresses.js +0 -77
  48. package/lib/cjs/screen-addresses/screen-addresses.d.ts +0 -50
  49. package/lib/cjs/screen-addresses/screen-addresses.js +0 -171
  50. package/lib/cjs/simulate/backends/tenderly-rest.d.ts +0 -20
  51. package/lib/cjs/simulate/backends/tenderly-rest.js +0 -279
  52. package/lib/esm/screen-addresses/index.d.ts +0 -1
  53. package/lib/esm/screen-addresses/index.js +0 -1
  54. package/lib/esm/screen-addresses/sanctioned-addresses.d.ts +0 -11
  55. package/lib/esm/screen-addresses/sanctioned-addresses.js +0 -74
  56. package/lib/esm/screen-addresses/screen-addresses.d.ts +0 -50
  57. package/lib/esm/screen-addresses/screen-addresses.js +0 -168
  58. package/lib/esm/simulate/backends/tenderly-rest.d.ts +0 -20
  59. 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 `TENDERLY_BUDGET_RATIO` (60%) of
27
- * the timeout. On `ExternalServiceError`, falls back to `eth_simulateV1` with
28
- * `max(remaining, FALLBACK_MIN_BUDGET_MS)` so the fallback still has a viable
29
- * window when Tenderly eats its full slice. `SimulationRevertedError` (contract
30
- * revert) propagates immediately without retry — a revert is a property of the
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, shareable } = params;
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.tenderlySupported && config.tenderlyRest) {
39
+ if (chain.tenderlyRpc) {
43
40
  const tenderlyTimeout = Math.floor(timeoutMs * TENDERLY_BUDGET_RATIO);
44
41
  try {
45
- return await (0, index_js_1.simulateTenderlyRest)({
46
- config: config.tenderlyRest,
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
- tenderlySupported: boolean;
3
+ tenderlyRpc?: TenderlyRpcConfig;
4
4
  simulateV1Url?: string;
5
5
  }
6
6
  /**
7
7
  * Stage 2 of the simulate() pipeline.
8
8
  *
9
- * Determines which backend(s) are available for a chainId. Throws
10
- * `UnsupportedChainError` when neither Tenderly nor `eth_simulateV1` is reachable
11
- * for the requested chain.
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
- * Determines which backend(s) are available for a chainId. Throws
9
- * `UnsupportedChainError` when neither Tenderly nor `eth_simulateV1` is reachable
10
- * for the requested chain.
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 tenderlySupported = config.tenderlyRest?.supportedChainIds.has(chainId) ?? false;
14
- const simulateV1Url = config.chains.get(chainId)?.simulateV1Url;
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 { tenderlySupported, simulateV1Url };
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 REST (primary) or `eth_simulateV1` (fallback) with a shared timeout
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` + `tenderlyUrl` → user-facing preview.
11
- * - `simulationTxs` + `transfers` → server-side verification before broadcast, then pass
12
- * both to `screenAddresses` for compliance.
13
- * - `calls[i]` → per-tx raw backend output (`logs`, `status`, `returnData`, `gasUsed`,
14
- * and Tenderly-only `assetChanges`). Aligned 1:1 with `simulationTxs[i]`.
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: Tenderly credentials, per-chain RPC URLs, optional
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 with
43
- * `txIdx`), and the optional `tenderlyUrl`.
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
- * { chains: new Map([[1, { simulateV1Url: rpcUrl }]]) },
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, options?: {
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 REST (primary) or `eth_simulateV1` (fallback) with a shared timeout
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` + `tenderlyUrl` → user-facing preview.
16
- * - `simulationTxs` + `transfers` → server-side verification before broadcast, then pass
17
- * both to `screenAddresses` for compliance.
18
- * - `calls[i]` → per-tx raw backend output (`logs`, `status`, `returnData`, `gasUsed`,
19
- * and Tenderly-only `assetChanges`). Aligned 1:1 with `simulationTxs[i]`.
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: Tenderly credentials, per-chain RPC URLs, optional
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 with
48
- * `txIdx`), and the optional `tenderlyUrl`.
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
- * { chains: new Map([[1, { simulateV1Url: rpcUrl }]]) },
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
- // biome-ignore lint/complexity/useMaxParams: TODO refactor to ≤2 params
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
- tenderlyUrl: result.tenderlyUrl,
89
+ assetChanges: result.assetChanges,
89
90
  };
90
91
  }
@@ -1,36 +1,34 @@
1
1
  import type { Address, BlockTag, Hex } from "viem";
2
2
  /**
3
- * Credentials and routing for the Tenderly REST simulation backend.
4
- * Omit from `SimulationConfig.tenderlyRest` to disable the Tenderly path entirely.
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 TenderlyRestConfig {
7
- /** e.g. "https://api.tenderly.co" */
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. Stored as a `Map` value in
17
- * `SimulationConfig.chains`, keyed by chain ID.
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 interface ChainSimulationConfig {
20
- /** JSON-RPC URL supporting `eth_simulateV1`. undefined = not available for this chain. */
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
- * At least one backend must be reachable for a given chainId — either Tenderly
27
- * (via `tenderlyRest` with that chainId in `TenderlyRestConfig.supportedChainIds`)
28
- * or `ChainSimulationConfig.simulateV1Url` in the `chains` map. Otherwise the
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, and (Tenderly only) assetChanges per tx.
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
- /** Shareable Tenderly URL. Present only when `shareable: true` and Tenderly ran (not fallback). */
110
- readonly tenderlyUrl?: string;
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
- tenderlyUrl?: string;
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 REST)
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, and (Tenderly only) asset changes per transaction.
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
  }
@@ -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";
@@ -1,4 +1,3 @@
1
- export { AddressScreeningError, BlacklistViolationError, ExternalServiceError, SimulationPackageError, SimulationRevertedError, SimulationValidationError, UnsupportedChainError, } from "./errors.js";
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, TenderlyRestConfig, Transfer, } from "./types.js";
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 { AddressScreeningError, BlacklistViolationError, ExternalServiceError, SimulationPackageError, SimulationRevertedError, SimulationValidationError, UnsupportedChainError, } from "./errors.js";
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 { calls: rawCalls };
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 { simulateTenderlyRest } from "./tenderly-rest.js";
2
+ export { simulateTenderlyRpc } from "./tenderly-rpc.js";