@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
package/README.md CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  ## Overview
4
4
 
5
- EVM transaction simulation engine for Morpho — bundle execution preview,
6
- transfer parsing, and compliance screening.
5
+ EVM transaction simulation engine for Morpho — bundle execution preview
6
+ and transfer parsing.
7
7
 
8
8
  ## Installation
9
9
 
@@ -22,38 +22,29 @@ Add to the consuming app's `package.json`:
22
22
  ```ts
23
23
  import {
24
24
  simulate,
25
- screenAddresses,
26
25
  type SimulationConfig,
27
26
  SimulationRevertedError
28
27
  } from "@morpho-org/evm-simulation";
29
28
 
30
29
  const config: SimulationConfig = {
31
- tenderlyRest: {
32
- apiBaseUrl: "https://api.tenderly.co",
33
- accessToken: process.env.TENDERLY_ACCESS_TOKEN!,
34
- accountSlug: "my-account",
35
- projectSlug: "my-project",
36
- supportedChainIds: new Set([1]),
37
- },
38
- chains: new Map([[1, { simulateV1Url: process.env.MAINNET_RPC_URL }]]),
30
+ chains: new Map([
31
+ [
32
+ 1,
33
+ {
34
+ tenderlyRpc: { rpcUrl: process.env.TENDERLY_RPC_URL! },
35
+ simulateV1Url: process.env.MAINNET_RPC_URL,
36
+ },
37
+ ],
38
+ ]),
39
39
  timeoutMs: 5000,
40
40
  };
41
41
 
42
42
  try {
43
- // Pass { shareable: true } when you want a shareable Tenderly URL (user-facing preview).
44
- // Omit it for server-side verification — the same function, same return shape.
45
- const { transfers, simulationTxs, tenderlyUrl } = await simulate(
46
- config,
47
- {
48
- chainId: 1,
49
- transactions: [{ from: user, to: vault, data: encodedDeposit }],
50
- authorizations: [{ type: "signature", token: usdc, spender: vault }],
51
- },
52
- { shareable: true }
53
- );
54
-
55
- // For server-side flows, pipe into screenAddresses:
56
- await screenAddresses({ simulationTxs, transfers });
43
+ const { transfers, simulationTxs } = await simulate(config, {
44
+ chainId: 1,
45
+ transactions: [{ from: user, to: vault, data: encodedDeposit }],
46
+ authorizations: [{ type: "signature", token: usdc, spender: vault }],
47
+ });
57
48
  } catch (err) {
58
49
  if (err instanceof SimulationRevertedError) {
59
50
  // show err.reason to the user
@@ -62,20 +53,21 @@ try {
62
53
  }
63
54
  ```
64
55
 
56
+ Each chain entry must declare at least one backend — `tenderlyRpc` (primary), `simulateV1Url` (fallback), or both. The type system enforces this.
57
+
65
58
  ### API surface
66
59
 
67
60
  All symbols below are re-exported from the package root.
68
61
 
69
- - `simulate(config, params, options?)` — run a bundle through the simulation pipeline. Pass `{ shareable: true }` for a shareable Tenderly URL; omit for server-side verification.
70
- - `screenAddresses({ simulationTxs, transfers, chainalysisApiKey? })` — static sanctioned-set + optional Chainalysis screening.
71
- - Config types: `SimulationConfig`, `TenderlyRestConfig`, `ChainSimulationConfig`, `SimulationLogger`.
62
+ - `simulate(config, params)` — run a bundle through the simulation pipeline.
63
+ - Config types: `SimulationConfig`, `TenderlyRpcConfig`, `ChainSimulationConfig`, `SimulationLogger`.
72
64
  - Data types: `SimulationTransaction`, `SimulationAuthorization`, `Transfer`, `SimulateParams`, `SimulationResult`.
73
- - Errors: `SimulationPackageError` (abstract base — `instanceof` it to catch any package error), `SimulationRevertedError`, `BlacklistViolationError`, `AddressScreeningError`, `ExternalServiceError`, `SimulationValidationError`, `UnsupportedChainError`.
65
+ - Errors: `SimulationPackageError` (abstract base — `instanceof` it to catch any package error), `SimulationRevertedError`, `BlacklistViolationError`, `ExternalServiceError`, `SimulationValidationError`, `UnsupportedChainError`.
74
66
 
75
67
  ### Deeper docs
76
68
 
77
69
  See [`CLAUDE.md`](./CLAUDE.md) in this directory for the execution flow diagram,
78
- backend tradeoffs, authorizations model, screening design, error-handling table,
70
+ backend tradeoffs, authorizations model, error-handling table,
79
71
  and recipes for adding a chain or a new backend.
80
72
 
81
73
  ## Development
@@ -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/cjs/errors.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.UnsupportedChainError = exports.SimulationValidationError = exports.ExternalServiceError = exports.AddressScreeningError = exports.BlacklistViolationError = exports.SimulationRevertedError = exports.SimulationPackageError = void 0;
3
+ exports.UnsupportedChainError = exports.SimulationValidationError = exports.ExternalServiceError = exports.BlacklistViolationError = exports.SimulationRevertedError = exports.SimulationPackageError = void 0;
4
4
  /**
5
5
  * Base class for every error this package throws. Transport-agnostic — no HTTP status codes.
6
6
  * Consumers pattern-match with `instanceof` on the concrete subclass.
@@ -34,16 +34,6 @@ class BlacklistViolationError extends SimulationPackageError {
34
34
  }
35
35
  }
36
36
  exports.BlacklistViolationError = BlacklistViolationError;
37
- /** Sanctioned or compromised address detected. Never bypassable. */
38
- class AddressScreeningError extends SimulationPackageError {
39
- addresses;
40
- code = "SCREENING_ERROR";
41
- constructor(message, addresses) {
42
- super(message);
43
- this.addresses = addresses;
44
- }
45
- }
46
- exports.AddressScreeningError = AddressScreeningError;
47
37
  /** Tenderly or RPC service is down. Bypassable — user can proceed. */
48
38
  class ExternalServiceError extends SimulationPackageError {
49
39
  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/cjs/index.js CHANGED
@@ -1,17 +1,14 @@
1
1
  "use strict";
2
2
  // Public fns (via feature-folder barrels)
3
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;
4
+ exports.simulate = exports.UnsupportedChainError = exports.SimulationValidationError = exports.SimulationRevertedError = exports.SimulationPackageError = exports.ExternalServiceError = exports.BlacklistViolationError = void 0;
5
5
  // Errors (for instanceof checks by consumers)
6
6
  var errors_js_1 = require("./errors.js");
7
- Object.defineProperty(exports, "AddressScreeningError", { enumerable: true, get: function () { return errors_js_1.AddressScreeningError; } });
8
7
  Object.defineProperty(exports, "BlacklistViolationError", { enumerable: true, get: function () { return errors_js_1.BlacklistViolationError; } });
9
8
  Object.defineProperty(exports, "ExternalServiceError", { enumerable: true, get: function () { return errors_js_1.ExternalServiceError; } });
10
9
  Object.defineProperty(exports, "SimulationPackageError", { enumerable: true, get: function () { return errors_js_1.SimulationPackageError; } });
11
10
  Object.defineProperty(exports, "SimulationRevertedError", { enumerable: true, get: function () { return errors_js_1.SimulationRevertedError; } });
12
11
  Object.defineProperty(exports, "SimulationValidationError", { enumerable: true, get: function () { return errors_js_1.SimulationValidationError; } });
13
12
  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; } });
13
+ var index_js_1 = require("./simulate/index.js");
14
+ Object.defineProperty(exports, "simulate", { enumerable: true, get: function () { return index_js_1.simulate; } });
@@ -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,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.groupAssetChanges = groupAssetChanges;
4
+ const viem_1 = require("viem");
5
+ // Locale-independent byte-order compare on lowercased hex, matching `sortTransfers`.
6
+ const compareAddress = (a, b) => {
7
+ const al = a.toLowerCase();
8
+ const bl = b.toLowerCase();
9
+ return al < bl ? -1 : al > bl ? 1 : 0;
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
+ function groupAssetChanges(entries) {
20
+ const byAccount = new Map();
21
+ for (const entry of entries) {
22
+ if (entry.diff === 0n)
23
+ continue;
24
+ const account = (0, viem_1.getAddress)(entry.account);
25
+ // `token` is normalized by callers (checksummed contract or `ethAddress`).
26
+ const token = entry.token;
27
+ let tokens = byAccount.get(account);
28
+ if (!tokens)
29
+ byAccount.set(account, (tokens = new Map()));
30
+ const prev = tokens.get(token);
31
+ tokens.set(token, {
32
+ token,
33
+ symbol: prev?.symbol ?? entry.symbol,
34
+ decimals: prev?.decimals ?? entry.decimals,
35
+ diff: (prev?.diff ?? 0n) + entry.diff,
36
+ });
37
+ }
38
+ return [...byAccount]
39
+ .map(([account, tokens]) => ({
40
+ account,
41
+ changes: [...tokens.values()]
42
+ .filter((c) => c.diff !== 0n)
43
+ .sort((x, y) => compareAddress(x.token, y.token)),
44
+ }))
45
+ .filter((a) => a.changes.length > 0)
46
+ .sort((a, b) => compareAddress(a.account, b.account));
47
+ }
@@ -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;
@@ -3,11 +3,22 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.simulateV1 = simulateV1;
4
4
  const viem_1 = require("viem");
5
5
  const errors_js_1 = require("../../errors.js");
6
+ const asset_changes_js_1 = require("../asset-changes.js");
7
+ const index_js_1 = require("../parsing/index.js");
6
8
  /**
7
9
  * Simulate transactions using eth_simulateV1 (viem simulateCalls).
8
10
  *
9
11
  * Uses stateOverride to set the user's ETH balance high enough to avoid
10
12
  * false "insufficient balance for gas" reverts.
13
+ *
14
+ * Derives `assetChanges` (net per-token deltas grouped by account) from the
15
+ * emitted transfer logs, plus native ETH from each transaction's top-level
16
+ * `value` (payer debited, recipient credited, under viem's `ethAddress`).
17
+ *
18
+ * Coverage on this path is therefore narrower than the Tenderly backend:
19
+ * native ETH moved through internal calls (e.g. a `WETH.withdraw` refund, a
20
+ * swap that pays out ETH) emits no log and is not reflected in `value`, so it
21
+ * is not captured. For full native-ETH accounting use Tenderly.
11
22
  */
12
23
  async function simulateV1(params) {
13
24
  const { rpcUrl, transactions, blockNumber, signal } = params;
@@ -64,7 +75,13 @@ async function simulateV1(params) {
64
75
  returnData: r.data ?? "0x",
65
76
  gasUsed: r.gasUsed,
66
77
  }));
67
- return { calls: rawCalls };
78
+ return {
79
+ calls: rawCalls,
80
+ assetChanges: toAssetChanges({
81
+ transfers: (0, index_js_1.parseTransfers)(rawCalls),
82
+ transactions,
83
+ }),
84
+ };
68
85
  }
69
86
  catch (error) {
70
87
  if (error instanceof errors_js_1.SimulationRevertedError)
@@ -74,3 +91,24 @@ async function simulateV1(params) {
74
91
  throw new errors_js_1.ExternalServiceError(`eth_simulateV1 error: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
75
92
  }
76
93
  }
94
+ /**
95
+ * Reduce parsed transfer logs and top-level transaction values to net per-token
96
+ * balance changes grouped by account. Native ETH is taken from the top-level
97
+ * `value`s (payer debited, recipient credited — internal moves are not logged,
98
+ * see `simulateV1` for the coverage caveat).
99
+ */
100
+ function toAssetChanges(params) {
101
+ const { transfers, transactions } = params;
102
+ const entries = [];
103
+ for (const tx of transactions) {
104
+ if (!tx.value)
105
+ continue;
106
+ entries.push({ account: tx.from, token: viem_1.ethAddress, diff: -tx.value });
107
+ entries.push({ account: tx.to, token: viem_1.ethAddress, diff: tx.value });
108
+ }
109
+ for (const { token, from, to, amount } of transfers) {
110
+ entries.push({ account: to, token, diff: amount });
111
+ entries.push({ account: from, token, diff: -amount });
112
+ }
113
+ return (0, asset_changes_js_1.groupAssetChanges)(entries);
114
+ }
@@ -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";
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.simulateTenderlyRest = exports.simulateV1 = void 0;
3
+ exports.simulateTenderlyRpc = exports.simulateV1 = void 0;
4
4
  var eth_simulate_v1_js_1 = require("./eth-simulate-v1.js");
5
5
  Object.defineProperty(exports, "simulateV1", { enumerable: true, get: function () { return eth_simulate_v1_js_1.simulateV1; } });
6
- var tenderly_rest_js_1 = require("./tenderly-rest.js");
7
- Object.defineProperty(exports, "simulateTenderlyRest", { enumerable: true, get: function () { return tenderly_rest_js_1.simulateTenderlyRest; } });
6
+ var tenderly_rpc_js_1 = require("./tenderly-rpc.js");
7
+ Object.defineProperty(exports, "simulateTenderlyRpc", { enumerable: true, get: function () { return tenderly_rpc_js_1.simulateTenderlyRpc; } });
@@ -0,0 +1,23 @@
1
+ import { type BlockTag } from "viem";
2
+ import type { RawSimulationResult, SimulationTransaction, TenderlyRpcConfig } from "../../types.js";
3
+ /**
4
+ * Simulate one or more transactions via Tenderly's Node Web3 Gateway.
5
+ * Dispatches to `tenderly_simulateTransaction` for a single call and
6
+ * `tenderly_simulateBundle` for a multi-call bundle.
7
+ *
8
+ * @param params.config - Tenderly RPC config (URL with access key embedded).
9
+ * @param params.transactions - Bundle to simulate, in execution order.
10
+ * @param params.blockNumber - Optional pinned block number or `BlockTag`. Defaults to `latest`.
11
+ * @param params.signal - Optional `AbortSignal` for cancellation / timeout.
12
+ * @returns A {@link RawSimulationResult} with one `RawCall` per input transaction.
13
+ * @throws {SimulationValidationError} when `transactions` is empty.
14
+ * @throws {SimulationRevertedError} when any simulated tx reports `status: false`.
15
+ * @throws {ExternalServiceError} on non-2xx response, JSON-RPC envelope error,
16
+ * schema-validation failure, or fetch-layer failure.
17
+ */
18
+ export declare function simulateTenderlyRpc(params: {
19
+ config: TenderlyRpcConfig;
20
+ transactions: SimulationTransaction[];
21
+ blockNumber?: bigint | BlockTag;
22
+ signal?: AbortSignal;
23
+ }): Promise<RawSimulationResult>;
@@ -0,0 +1,230 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.simulateTenderlyRpc = simulateTenderlyRpc;
4
+ const viem_1 = require("viem");
5
+ const zod_1 = require("zod");
6
+ const errors_js_1 = require("../../errors.js");
7
+ const asset_changes_js_1 = require("../asset-changes.js");
8
+ const addressSchema = zod_1.z.custom((val) => typeof val === "string" && (0, viem_1.isAddress)(val));
9
+ const hexSchema = zod_1.z.custom((val) => typeof val === "string" && (0, viem_1.isHex)(val));
10
+ const traceFrameSchema = zod_1.z
11
+ .object({
12
+ output: hexSchema.optional(),
13
+ error: zod_1.z.string().optional(),
14
+ errorReason: zod_1.z.string().optional(),
15
+ })
16
+ .passthrough();
17
+ const logSchema = zod_1.z
18
+ .object({
19
+ raw: zod_1.z
20
+ .object({
21
+ address: addressSchema,
22
+ topics: zod_1.z.array(hexSchema).optional(),
23
+ data: hexSchema.optional(),
24
+ })
25
+ .passthrough()
26
+ .optional(),
27
+ })
28
+ .passthrough();
29
+ const assetChangeSchema = zod_1.z
30
+ .object({
31
+ assetInfo: zod_1.z
32
+ .object({
33
+ contractAddress: addressSchema.optional(),
34
+ symbol: zod_1.z.string().optional(),
35
+ decimals: zod_1.z.number().optional(),
36
+ })
37
+ .passthrough(),
38
+ from: addressSchema.optional(),
39
+ to: addressSchema.optional(),
40
+ rawAmount: hexSchema,
41
+ })
42
+ .passthrough();
43
+ const simResultSchema = zod_1.z
44
+ .object({
45
+ status: zod_1.z.boolean(),
46
+ gasUsed: hexSchema,
47
+ logs: zod_1.z.array(logSchema).optional(),
48
+ trace: zod_1.z.array(traceFrameSchema).optional(),
49
+ assetChanges: zod_1.z.array(assetChangeSchema).optional(),
50
+ error: zod_1.z.string().optional(),
51
+ errorMessage: zod_1.z.string().optional(),
52
+ })
53
+ .passthrough();
54
+ const rpcErrorSchema = zod_1.z
55
+ .object({
56
+ code: zod_1.z.number().optional(),
57
+ message: zod_1.z.string(),
58
+ data: zod_1.z.unknown().optional(),
59
+ })
60
+ .passthrough();
61
+ function rpcEnvelope(result) {
62
+ return zod_1.z
63
+ .object({ result: result.optional(), error: rpcErrorSchema.optional() })
64
+ .passthrough();
65
+ }
66
+ const singleEnvelope = rpcEnvelope(simResultSchema);
67
+ const bundleEnvelope = rpcEnvelope(zod_1.z.array(simResultSchema).min(1));
68
+ /**
69
+ * Simulate one or more transactions via Tenderly's Node Web3 Gateway.
70
+ * Dispatches to `tenderly_simulateTransaction` for a single call and
71
+ * `tenderly_simulateBundle` for a multi-call bundle.
72
+ *
73
+ * @param params.config - Tenderly RPC config (URL with access key embedded).
74
+ * @param params.transactions - Bundle to simulate, in execution order.
75
+ * @param params.blockNumber - Optional pinned block number or `BlockTag`. Defaults to `latest`.
76
+ * @param params.signal - Optional `AbortSignal` for cancellation / timeout.
77
+ * @returns A {@link RawSimulationResult} with one `RawCall` per input transaction.
78
+ * @throws {SimulationValidationError} when `transactions` is empty.
79
+ * @throws {SimulationRevertedError} when any simulated tx reports `status: false`.
80
+ * @throws {ExternalServiceError} on non-2xx response, JSON-RPC envelope error,
81
+ * schema-validation failure, or fetch-layer failure.
82
+ */
83
+ async function simulateTenderlyRpc(params) {
84
+ const { config, transactions, blockNumber, signal } = params;
85
+ const firstTx = transactions[0];
86
+ if (!firstTx) {
87
+ throw new errors_js_1.SimulationValidationError("At least one transaction is required", []);
88
+ }
89
+ const block = encodeBlock(blockNumber);
90
+ // Inflate sender ETH balance to avoid false "insufficient funds for gas"
91
+ // reverts on wallets low on native gas token — mirrors simulateV1.
92
+ const stateOverrides = buildStateOverrides(firstTx.from);
93
+ try {
94
+ if (transactions.length === 1) {
95
+ const json = await rpcRequest({
96
+ rpcUrl: config.rpcUrl,
97
+ method: "tenderly_simulateTransaction",
98
+ params: [buildCall(firstTx), block, stateOverrides],
99
+ signal,
100
+ });
101
+ const result = unwrapResult(singleEnvelope.parse(json));
102
+ return {
103
+ calls: [toRawCall(result)],
104
+ assetChanges: toAssetChanges([result]),
105
+ };
106
+ }
107
+ const json = await rpcRequest({
108
+ rpcUrl: config.rpcUrl,
109
+ method: "tenderly_simulateBundle",
110
+ params: [transactions.map(buildCall), block, stateOverrides],
111
+ signal,
112
+ });
113
+ const results = unwrapResult(bundleEnvelope.parse(json));
114
+ return {
115
+ calls: results.map(toRawCall),
116
+ assetChanges: toAssetChanges(results),
117
+ };
118
+ }
119
+ catch (error) {
120
+ if (error instanceof errors_js_1.SimulationRevertedError)
121
+ throw error;
122
+ if (error instanceof errors_js_1.ExternalServiceError)
123
+ throw error;
124
+ throw new errors_js_1.ExternalServiceError(`Tenderly RPC error: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
125
+ }
126
+ }
127
+ async function rpcRequest(params) {
128
+ const { rpcUrl, method, params: rpcParams, signal } = params;
129
+ const response = await fetch(rpcUrl, {
130
+ method: "POST",
131
+ headers: { "Content-Type": "application/json" },
132
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params: rpcParams }),
133
+ signal,
134
+ });
135
+ if (!response.ok) {
136
+ throw new errors_js_1.ExternalServiceError(`Tenderly RPC returned ${response.status}: ${response.statusText}`);
137
+ }
138
+ return await response.json();
139
+ }
140
+ function unwrapResult(envelope) {
141
+ if (envelope.error) {
142
+ throw new errors_js_1.ExternalServiceError(`Tenderly RPC error: ${envelope.error.message}`);
143
+ }
144
+ if (envelope.result === undefined) {
145
+ throw new errors_js_1.ExternalServiceError("Tenderly RPC returned no result");
146
+ }
147
+ return envelope.result;
148
+ }
149
+ function buildCall(tx) {
150
+ return {
151
+ from: tx.from,
152
+ to: tx.to,
153
+ input: tx.data,
154
+ data: tx.data,
155
+ value: (0, viem_1.numberToHex)(tx.value ?? 0n),
156
+ };
157
+ }
158
+ function buildStateOverrides(sender) {
159
+ return { [sender]: { balance: (0, viem_1.numberToHex)(viem_1.maxUint256) } };
160
+ }
161
+ function encodeBlock(blockNumber) {
162
+ if (blockNumber === undefined)
163
+ return "latest";
164
+ return typeof blockNumber === "bigint"
165
+ ? (0, viem_1.numberToHex)(blockNumber)
166
+ : blockNumber;
167
+ }
168
+ function toRawCall(data) {
169
+ if (data.status !== true) {
170
+ // Tenderly RPC surfaces the revert reason on the trace frame; the
171
+ // top-level fields are checked as a defensive fallback.
172
+ const traceError = data.trace?.find((f) => f.errorReason || f.error);
173
+ const message = traceError?.errorReason ||
174
+ traceError?.error ||
175
+ data.errorMessage ||
176
+ data.error ||
177
+ "Transaction simulation reverted";
178
+ throw new errors_js_1.SimulationRevertedError(message, data);
179
+ }
180
+ const logs = [];
181
+ for (const log of data.logs ?? []) {
182
+ if (log.raw) {
183
+ logs.push({
184
+ address: log.raw.address,
185
+ topics: log.raw.topics ?? [],
186
+ data: log.raw.data ?? "0x",
187
+ });
188
+ }
189
+ }
190
+ return {
191
+ logs,
192
+ status: true,
193
+ returnData: data.trace?.[0]?.output ?? "0x",
194
+ gasUsed: BigInt(data.gasUsed),
195
+ };
196
+ }
197
+ /**
198
+ * Reduce Tenderly's per-transfer asset changes to net per-token balance changes
199
+ * grouped by account. Both endpoints of each transfer are accounted (sender,
200
+ * counterparties, and the zero address for mints/burns).
201
+ */
202
+ function toAssetChanges(results) {
203
+ const entries = [];
204
+ for (const result of results) {
205
+ for (const change of result.assetChanges ?? []) {
206
+ const amount = BigInt(change.rawAmount);
207
+ const token = change.assetInfo.contractAddress
208
+ ? (0, viem_1.getAddress)(change.assetInfo.contractAddress)
209
+ : viem_1.ethAddress;
210
+ const { symbol, decimals } = change.assetInfo;
211
+ if (change.to)
212
+ entries.push({
213
+ account: change.to,
214
+ token,
215
+ diff: amount,
216
+ symbol,
217
+ decimals,
218
+ });
219
+ if (change.from)
220
+ entries.push({
221
+ account: change.from,
222
+ token,
223
+ diff: -amount,
224
+ symbol,
225
+ decimals,
226
+ });
227
+ }
228
+ }
229
+ return (0, asset_changes_js_1.groupAssetChanges)(entries);
230
+ }
@@ -145,7 +145,9 @@ function parseTransfers(calls, logger) {
145
145
  catch (error) {
146
146
  // Safety net for anything that slips past the length checks above
147
147
  // (e.g., viem checksum throws on non-hex input). Loud enough to diagnose.
148
- warnMalformed(logger, log, error instanceof Error ? error.message : String(error));
148
+ warnMalformed(logger, log,
149
+ /* v8 ignore next: parser paths throw Error instances for valid RawLog shapes. */
150
+ error instanceof Error ? error.message : String(error));
149
151
  }
150
152
  }
151
153
  }
@@ -5,21 +5,17 @@ import type { RawSimulationResult, SimulationConfig, SimulationTransaction } fro
5
5
  *
6
6
  * Dispatches the transaction bundle to the available backend with a shared timeout
7
7
  * budget:
8
- * - If Tenderly is configured for the chain, it gets `TENDERLY_BUDGET_RATIO` (60%) of
9
- * the timeout. On `ExternalServiceError`, falls back to `eth_simulateV1` with
10
- * `max(remaining, FALLBACK_MIN_BUDGET_MS)` so the fallback still has a viable
11
- * window when Tenderly eats its full slice. `SimulationRevertedError` (contract
12
- * revert) propagates immediately without retry — a revert is a property of the
13
- * bundle, not the backend.
8
+ * - If a Tenderly RPC URL is configured for the chain, it gets
9
+ * `TENDERLY_BUDGET_RATIO` (60%) of the timeout. On `ExternalServiceError`,
10
+ * falls back to `eth_simulateV1` with `max(remaining, FALLBACK_MIN_BUDGET_MS)`
11
+ * so the fallback still has a viable window when Tenderly eats its full slice.
12
+ * `SimulationRevertedError` (contract revert) propagates immediately without
13
+ * retry — a revert is a property of the bundle, not the backend.
14
14
  * - If only `eth_simulateV1` is configured, it gets the full timeout.
15
- *
16
- * `shareable` is Tenderly-only: when true, the backend persists the simulation and
17
- * returns a shareable URL. `eth_simulateV1` ignores it (no persistence concept).
18
15
  */
19
16
  export declare function executeSimulation(params: {
20
17
  config: SimulationConfig;
21
18
  chainId: number;
22
19
  transactions: SimulationTransaction[];
23
20
  blockNumber?: bigint | BlockTag;
24
- shareable: boolean;
25
21
  }): Promise<RawSimulationResult>;