viem-tx-sim 0.1.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 (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +228 -0
  3. package/contracts/TxSimulator.sol +305 -0
  4. package/dist/constants.d.ts +15 -0
  5. package/dist/constants.d.ts.map +1 -0
  6. package/dist/constants.js +15 -0
  7. package/dist/constants.js.map +1 -0
  8. package/dist/errors.d.ts +35 -0
  9. package/dist/errors.d.ts.map +1 -0
  10. package/dist/errors.js +39 -0
  11. package/dist/errors.js.map +1 -0
  12. package/dist/generated/txSimulatorBytecode.d.ts +2 -0
  13. package/dist/generated/txSimulatorBytecode.d.ts.map +1 -0
  14. package/dist/generated/txSimulatorBytecode.js +3 -0
  15. package/dist/generated/txSimulatorBytecode.js.map +1 -0
  16. package/dist/index.d.ts +5 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +4 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/internal/data.d.ts +8 -0
  21. package/dist/internal/data.d.ts.map +1 -0
  22. package/dist/internal/data.js +31 -0
  23. package/dist/internal/data.js.map +1 -0
  24. package/dist/internal/probes.d.ts +29 -0
  25. package/dist/internal/probes.d.ts.map +1 -0
  26. package/dist/internal/probes.js +159 -0
  27. package/dist/internal/probes.js.map +1 -0
  28. package/dist/internal/requirements.d.ts +5 -0
  29. package/dist/internal/requirements.d.ts.map +1 -0
  30. package/dist/internal/requirements.js +173 -0
  31. package/dist/internal/requirements.js.map +1 -0
  32. package/dist/internal/rpc.d.ts +36 -0
  33. package/dist/internal/rpc.d.ts.map +1 -0
  34. package/dist/internal/rpc.js +124 -0
  35. package/dist/internal/rpc.js.map +1 -0
  36. package/dist/internal/simulator.d.ts +33 -0
  37. package/dist/internal/simulator.d.ts.map +1 -0
  38. package/dist/internal/simulator.js +199 -0
  39. package/dist/internal/simulator.js.map +1 -0
  40. package/dist/internal/slots.d.ts +7 -0
  41. package/dist/internal/slots.d.ts.map +1 -0
  42. package/dist/internal/slots.js +129 -0
  43. package/dist/internal/slots.js.map +1 -0
  44. package/dist/txSimulator.d.ts +83 -0
  45. package/dist/txSimulator.d.ts.map +1 -0
  46. package/dist/txSimulator.js +75 -0
  47. package/dist/txSimulator.js.map +1 -0
  48. package/dist/types.d.ts +214 -0
  49. package/dist/types.d.ts.map +1 -0
  50. package/dist/types.js +2 -0
  51. package/dist/types.js.map +1 -0
  52. package/package.json +82 -0
  53. package/src/constants.ts +15 -0
  54. package/src/errors.ts +45 -0
  55. package/src/generated/txSimulatorBytecode.ts +5 -0
  56. package/src/index.ts +34 -0
  57. package/src/internal/data.ts +36 -0
  58. package/src/internal/probes.ts +221 -0
  59. package/src/internal/requirements.ts +240 -0
  60. package/src/internal/rpc.ts +207 -0
  61. package/src/internal/simulator.ts +306 -0
  62. package/src/internal/slots.ts +211 -0
  63. package/src/txSimulator.ts +176 -0
  64. package/src/types.ts +240 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 frontier159
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,228 @@
1
+ # viem-tx-sim
2
+
3
+ RPC-only transaction simulation helpers for [viem](https://viem.sh) applications: preview the asset changes of a transaction (or an ERC-5792 batch) before anyone signs it, using nothing but standard JSON-RPC.
4
+
5
+ ## Motivation
6
+
7
+ Credit to the [apoorv X thread](https://x.com/apoorveth/status/2041544070481449266), transcribed in [motivation.md](https://github.com/frontier159/viem-tx-sim/blob/main/docs/motivation.md).
8
+
9
+ Every wallet shows "asset changes" before you sign. Most do it by sending your data to a centralized simulation API — a single point of failure and a privacy leak. viem-tx-sim makes the EVM do the work itself:
10
+
11
+ 1. `eth_createAccessList` dry-runs each call and returns every contract the transaction touches — those become candidate tokens, with no token lists or indexers.
12
+ 2. One `eth_call` with state overrides injects a never-deployed `TxSimulator` contract **at the user's own address** and executes the calls. Because the simulator runs as the user, `address(this)` and `msg.sender` are the real account, so balance reads, allowance checks, and `msg.sender`-gated logic behave exactly as they would in the real transaction. Batch calls run sequentially in one EVM context, so an approval in call 1 is visible to a swap in call 2.
13
+
14
+ That is two RPC calls for a single-call transaction, or N + 1 calls for an N-call batch. Zero servers, zero trust assumptions. See [docs/motivation.md](https://github.com/frontier159/viem-tx-sim/blob/main/docs/motivation.md) for the design's origin story, including how Permit2's ERC-1271 path and proxy-token storage are handled.
15
+
16
+ ## Getting started
17
+
18
+ ```sh
19
+ pnpm add viem-tx-sim viem
20
+ ```
21
+
22
+ This package is ESM-only (no CommonJS build) and requires Node 20 or newer. `viem` is a peer dependency, so install it alongside `viem-tx-sim` as shown above.
23
+
24
+ Pre-release consumers can install from git with `pnpm add github:frontier159/viem-tx-sim`; the `prepare` script builds `dist/` with `tsc` from committed contract bytecode, so Foundry is not needed at install time.
25
+
26
+ Simulate depositing 1,000 USDS into the sUSDS ERC-4626 vault on mainnet — an approve followed by a deposit, as one atomic batch:
27
+
28
+ ```ts
29
+ import { createPublicClient, encodeFunctionData, http, parseAbi, parseUnits } from "viem";
30
+ import { mainnet } from "viem/chains";
31
+ import { DEFAULT_SIMULATION_GAS_LIMIT, TxSimulator } from "viem-tx-sim";
32
+
33
+ const USDS = "0xdC035D45d973E3EC169d2276DDab16f1e407384F";
34
+ const SUSDS = "0xa3931d71877C0E7a3148CB7Eb4463524FEc27fbD";
35
+
36
+ const client = createPublicClient({ chain: mainnet, transport: http(RPC_URL) });
37
+ const sim = TxSimulator.create({ client, gas: DEFAULT_SIMULATION_GAS_LIMIT });
38
+
39
+ const user = "0xYourAddress"; // no key or signing involved — any address can be simulated
40
+ const assets = parseUnits("1000", 18);
41
+
42
+ const result = await sim.simulate({
43
+ from: user,
44
+ calls: [
45
+ {
46
+ to: USDS,
47
+ data: encodeFunctionData({
48
+ abi: parseAbi(["function approve(address spender, uint256 amount) returns (bool)"]),
49
+ functionName: "approve",
50
+ args: [SUSDS, assets],
51
+ }),
52
+ },
53
+ {
54
+ to: SUSDS,
55
+ data: encodeFunctionData({
56
+ abi: parseAbi(["function deposit(uint256 assets, address receiver) returns (uint256 shares)"]),
57
+ functionName: "deposit",
58
+ args: [assets, user],
59
+ }),
60
+ },
61
+ ],
62
+ });
63
+
64
+ console.log(result.status); // "success"
65
+ console.log(result.assetBalanceDeltas);
66
+ // [
67
+ // { asset: "0xdC03...384F", delta: -1000000000000000000000n }, // 1,000 USDS out
68
+ // { asset: "0xa393...7fbD", delta: 9xx...n }, // sUSDS shares in
69
+ // ]
70
+ ```
71
+
72
+ Deltas are raw `bigint` amounts in each token's own units, observed from chain state alone. A revert is returned as `status: "reverted"`, never thrown; checking `status` gives typed access to `revertData` and `failingCallIndex`.
73
+
74
+ `sim.simulate()` runs against the account's real balances and does not retry or forge state by itself. If `user` doesn't actually hold 1,000 USDS (say you're previewing for a view-only address), prepare and pass a balance override — see the next section. `DEFAULT_SIMULATION_GAS_LIMIT` is exported for callers that want to pass or display the default 16M simulation gas budget.
75
+
76
+ ## Preparing balance and allowance overrides
77
+
78
+ Override preparation is explicit and cacheable. Preparation returns ready-to-use `TokenSlotOverride[]` entries, so pass them into a single simulation run:
79
+
80
+ ```ts
81
+ import { TxSimulator } from "viem-tx-sim";
82
+
83
+ const sim = TxSimulator.create({ client });
84
+ const balanceOverrides = await sim.prepareBalanceOverrides({
85
+ from,
86
+ tokens: [token],
87
+ });
88
+ const allowanceOverrides = await sim.prepareAllowanceOverrides({
89
+ from,
90
+ pairs: [{ token, spender }],
91
+ });
92
+
93
+ const result = await sim.simulate({
94
+ from,
95
+ calls: [{ to, data }],
96
+ tokenSlotOverrides: [...balanceOverrides.slots, ...allowanceOverrides.slots],
97
+ });
98
+ ```
99
+
100
+ Prepared balance overrides are reusable per token/owner, and prepared allowance overrides are reusable per token/owner/spender for the block/state you trust. Preparation pre-sets `amount` to the exported `OVERRIDE_TOKEN_AMOUNT` sentinel, a non-max `10^50` value chosen so standard ERC-20 allowance decrements remain observable. Handcrafted override amounts must be below `uint256.max`; max allowance skips decrements, and max balances can overflow on incoming transfers. Deltas for real holdings still work for rebasing tokens like stETH; only dealing hypothetical balances can fail, reported in `unresolved`.
101
+
102
+ ## Estimating asset requirements (optional)
103
+
104
+ When you don't already know which balances and approvals a transaction needs, `sim.estimateAssetRequirements()` measures them by forging generous state and observing per-call balance and allowance changes. Returned amounts are estimates measured under forged state and should be padded; pairs whose allowance is set inside the batch (approve or permit) are excluded, and measured allowance decreases are sanity-bounded by the token's gross outflow. Measurements discarded by that bound are reported under `unresolved.allowances`.
105
+
106
+ ```ts
107
+ import { TxSimulator } from "viem-tx-sim";
108
+
109
+ const sim = TxSimulator.create({ client });
110
+ const requirements = await sim.estimateAssetRequirements({ from, calls });
111
+ // requirements.allowances -> [{ token, spender, amount }]
112
+ // requirements.balances -> [{ token, amount }]
113
+ // requirements.slots -> feed to sim.simulate({ ..., tokenSlotOverrides })
114
+
115
+ const result = await sim.simulate({
116
+ from,
117
+ calls,
118
+ tokenSlotOverrides: requirements.slots,
119
+ });
120
+ ```
121
+
122
+ ## Debugging
123
+
124
+ Enable logging per simulation call:
125
+
126
+ ```ts
127
+ import { TxSimulator } from "viem-tx-sim";
128
+
129
+ const sim = TxSimulator.create({ client });
130
+ const result = await sim.simulate({
131
+ from,
132
+ calls: [{ to, data, value: 0n }],
133
+ debug: true,
134
+ });
135
+ ```
136
+
137
+ Or pass a callback to collect structured events:
138
+
139
+ ```ts
140
+ await sim.simulate({
141
+ from,
142
+ calls: [{ to, data }],
143
+ debug: (event) => {
144
+ console.debug(event.method, event.step, event.phase, event.durationMs);
145
+ },
146
+ });
147
+ ```
148
+
149
+ ## Decoding Reverts
150
+
151
+ Reverted simulations always return raw `revertData` and, when present, a `revertSelector`.
152
+ Pass custom errors as `errorAbi` on `TxSimulator.create({ client, errorAbi })` or on a single `sim.simulate({ ..., errorAbi })` call to populate `revertError` and human-readable `revertReason`.
153
+ For example: `errorAbi: parseAbi(["error InsufficientBalance(uint256 have, uint256 want)"])`.
154
+
155
+ ## Known limitations
156
+
157
+ Situations the simulation does not cover, or where the preview can differ from real execution. None of these throw — they show up as wrong or missing deltas, or as a simulated revert where the real transaction would succeed (or vice versa).
158
+
159
+ **The account has code during simulation.** Injecting `TxSimulator` at `from` is the core trick, and it is visible on-chain logic:
160
+
161
+ - Contracts that gate on "is the caller an EOA" via `extcodesize(msg.sender) == 0` see a contract during simulation and may take a different branch than the real transaction.
162
+ - Receiving ERC-721/1155 tokens via `safeTransferFrom`/`safeMint` succeeds: `TxSimulator` implements `onERC721Received`, `onERC1155Received`, and `onERC1155BatchReceived`, so safe transfers into the simulated account match real execution for EOAs and contract wallets.
163
+ - ERC-777 `send` to the simulated account reverts unless the account has a real ERC-1820 registration on-chain.
164
+ - Permit2-style signature checks are handled for EOAs: the injected `isValidSignature` performs the same ECDSA recovery the real `ecrecover` path would.
165
+
166
+ **Smart-contract wallets (e.g. a Gnosis Safe) are treated as plain senders.** Using a Safe as `from` works: the code override replaces the wallet's bytecode but keeps its storage, ETH, and token balances, and every call executes with `msg.sender` = the wallet. What is *not* modeled is the wallet itself:
167
+
168
+ - The injected `isValidSignature` **replaces the wallet's own ERC-1271 validation** and only accepts EOA-style signatures recovering to `from`. Flows that require the wallet's real contract-signature logic (Permit2 permits or orders signed by the Safe itself) simulate as reverted. This is intentional — the goal is simulating downstream protocol behavior, not the wallet's signing machinery.
169
+ - Guards, modules, owner thresholds, nonces, and `operation=DELEGATECALL` batches are outside the simulation. A transaction guard that would block the real execution is invisible to the preview.
170
+ - `tx.origin` is the `from` address during simulation; in real execution it is the submitting EOA.
171
+
172
+ **An adversarial contract can detect it is being simulated** — via the code at `from`, the recognizable forged balances, or `eth_call` context — and behave differently in the real transaction. This is inherent to state-override simulation (centralized simulation APIs share it). Treat the preview as best-effort insight, not a security guarantee against malicious contracts.
173
+
174
+ **Results are estimates against one block's state.** Deltas and estimated asset requirements reflect the chosen block; prices, liquidity, and allowances move before the real transaction lands. Pad amounts accordingly. Amounts from `sim.estimateAssetRequirements()` are additionally measured under forged (very large) balances, so contracts that branch on the account's real balance can be measured on the wrong branch.
175
+
176
+ **Asset coverage is native + `balanceOf(address)`.** Deltas track ETH and anything answering ERC-20-style `balanceOf` (an ERC-721 shows up as a count delta, without token IDs). ERC-1155 balances (`balanceOf(address,uint256)`) are not tracked. Tokens whose balance is computed rather than stored in one slot per holder (rebasing/share-based tokens like stETH) cannot be forged — override preparation verifies slots before writing and reports them in the `unresolved` list.
177
+
178
+ **Candidate discovery follows the dry run.** Token candidates come from `eth_createAccessList` on the *unforged* calls; if that dry run reverts early, contracts that would only be touched later are not discovered, and their deltas are missed. Forging (or `estimateAssetRequirements()`, which measures after forging) avoids most of this.
179
+
180
+ **RPC provider requirements.** The provider must support `eth_createAccessList` (including returning the access list for reverting calls) and `eth_call` with state overrides. Missing support surfaces as `AccessListUnsupportedError` / `StateOverrideUnsupportedError`.
181
+
182
+ **Not a gas estimator.** The simulation runs under `DEFAULT_SIMULATION_GAS_LIMIT` by default and the injected code changes gas accounting; use `eth_estimateGas` on the real transaction for gas.
183
+
184
+ ## Development
185
+
186
+ Use Node.js 20 or newer with pnpm 10. With `proto`:
187
+
188
+ ```sh
189
+ proto use pnpm 10.18.3 --pin
190
+ pnpm install
191
+ ```
192
+
193
+ Or, if your Node installation has Corepack:
194
+
195
+ ```sh
196
+ corepack enable
197
+ corepack prepare pnpm@10.18.3 --activate
198
+ pnpm install
199
+ ```
200
+
201
+ If your version manager still selects pnpm 11 under Node 20, either switch pnpm to 10 or switch Node to 22.13+.
202
+
203
+ Building and testing requires [Foundry](https://getfoundry.sh) (`forge` compiles `TxSimulator.sol`, `anvil` backs the test suite):
204
+
205
+ ```sh
206
+ pnpm build
207
+ pnpm test
208
+ ```
209
+
210
+ Run `pnpm verify` to execute the full local gate that CI runs: lint, typecheck, build, and tests.
211
+
212
+ To see every RPC call the simulator makes during tests:
213
+
214
+ ```sh
215
+ pnpm test:debug
216
+ ```
217
+
218
+ To run the opt-in mainnet RPC integration test:
219
+
220
+ ```sh
221
+ MAINNET_RPC_URL=$MAINNET_RPC_URL pnpm test:mainnet
222
+ ```
223
+
224
+ Set `MAINNET_BLOCK_NUMBER` to override the pinned default block.
225
+
226
+ ## Scope
227
+
228
+ V1 returns raw balance deltas only. Token metadata, token lists, indexers, centralized simulation APIs, approval UX, and price enrichment are intentionally out of scope.
@@ -0,0 +1,305 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.24;
3
+
4
+ import {IERC1271Like} from "./interfaces/IERC1271Like.sol";
5
+
6
+ contract TxSimulator is IERC1271Like {
7
+ bytes4 internal constant ERC1271_MAGIC_VALUE = 0x1626ba7e;
8
+ bytes4 internal constant ERC1271_INVALID_VALUE = 0xffffffff;
9
+ bytes4 internal constant BALANCE_OF_SELECTOR = 0x70a08231;
10
+ bytes4 internal constant ALLOWANCE_SELECTOR = 0xdd62ed3e;
11
+ uint256 internal constant MAX_INT256 = 2 ** 255 - 1;
12
+
13
+ struct SimulatedCall {
14
+ address to;
15
+ uint256 value;
16
+ bytes data;
17
+ }
18
+
19
+ struct AllowanceProbe {
20
+ address token;
21
+ address spender;
22
+ }
23
+
24
+ struct SimulationResult {
25
+ bool success;
26
+ uint256 failingCallIndex;
27
+ bytes revertData;
28
+ int256 nativeDelta;
29
+ address[] observedTokens;
30
+ address[] deltaTokens;
31
+ int256[] tokenDeltas;
32
+ uint256[] maxTokenOutflows;
33
+ uint256 maxNativeOutflow;
34
+ uint256[] allowanceCheckpoints;
35
+ }
36
+
37
+ struct TokenState {
38
+ uint256[] beforeBalances;
39
+ uint256[] minBalances;
40
+ bool[] isToken;
41
+ address[] observedScratch;
42
+ uint256 observedCount;
43
+ }
44
+
45
+ struct ExecutionState {
46
+ bool[] isToken;
47
+ uint256[] minBalances;
48
+ uint256[] checkpoints;
49
+ uint256 stride;
50
+ }
51
+
52
+ function simulate(SimulatedCall[] calldata calls, address[] calldata candidates, AllowanceProbe[] calldata probes)
53
+ external
54
+ returns (SimulationResult memory result)
55
+ {
56
+ uint256 nativeBefore = address(this).balance;
57
+ TokenState memory tokenState = _snapshotTokens(candidates);
58
+
59
+ result.allowanceCheckpoints = new uint256[](probes.length * (calls.length + 1));
60
+ ExecutionState memory executionState = ExecutionState({
61
+ isToken: tokenState.isToken,
62
+ minBalances: tokenState.minBalances,
63
+ checkpoints: result.allowanceCheckpoints,
64
+ stride: calls.length + 1
65
+ });
66
+ uint256 nativeMin;
67
+ (result.success, result.failingCallIndex, result.revertData, nativeMin) =
68
+ _executeCalls(calls, candidates, probes, executionState, nativeBefore);
69
+
70
+ result.nativeDelta = _signedDelta(address(this).balance, nativeBefore);
71
+ result.observedTokens = _trimAddresses(tokenState.observedScratch, tokenState.observedCount);
72
+ result.maxNativeOutflow = nativeBefore >= nativeMin ? nativeBefore - nativeMin : 0;
73
+ _writeTokenResults(candidates, tokenState, result);
74
+ }
75
+
76
+ function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4) {
77
+ return _recover(hash, signature) == address(this) ? ERC1271_MAGIC_VALUE : ERC1271_INVALID_VALUE;
78
+ }
79
+
80
+ function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) {
81
+ return 0x150b7a02;
82
+ }
83
+
84
+ function onERC1155Received(address, address, uint256, uint256, bytes calldata) external pure returns (bytes4) {
85
+ return 0xf23a6e61;
86
+ }
87
+
88
+ function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata)
89
+ external
90
+ pure
91
+ returns (bytes4)
92
+ {
93
+ return 0xbc197c81;
94
+ }
95
+
96
+ receive() external payable {}
97
+
98
+ function _snapshotTokens(address[] calldata candidates) internal view returns (TokenState memory tokenState) {
99
+ tokenState.beforeBalances = new uint256[](candidates.length);
100
+ tokenState.minBalances = new uint256[](candidates.length);
101
+ tokenState.isToken = new bool[](candidates.length);
102
+ tokenState.observedScratch = new address[](candidates.length);
103
+
104
+ for (uint256 i = 0; i < candidates.length; ++i) {
105
+ (bool ok, uint256 balance) = _tryBalanceOf(candidates[i], address(this));
106
+ if (ok) {
107
+ tokenState.isToken[i] = true;
108
+ tokenState.beforeBalances[i] = balance;
109
+ tokenState.minBalances[i] = balance;
110
+ tokenState.observedScratch[tokenState.observedCount++] = candidates[i];
111
+ }
112
+ }
113
+ }
114
+
115
+ function _writeTokenResults(
116
+ address[] calldata candidates,
117
+ TokenState memory tokenState,
118
+ SimulationResult memory result
119
+ ) internal view {
120
+ result.maxTokenOutflows = new uint256[](candidates.length);
121
+ address[] memory deltaTokensScratch = new address[](candidates.length);
122
+ int256[] memory tokenDeltasScratch = new int256[](candidates.length);
123
+ uint256 deltaCount = 0;
124
+
125
+ for (uint256 i = 0; i < candidates.length; ++i) {
126
+ if (!tokenState.isToken[i]) continue;
127
+
128
+ if (tokenState.beforeBalances[i] >= tokenState.minBalances[i]) {
129
+ result.maxTokenOutflows[i] = tokenState.beforeBalances[i] - tokenState.minBalances[i];
130
+ }
131
+
132
+ (bool ok, uint256 afterBalance) = _tryBalanceOf(candidates[i], address(this));
133
+ if (!ok) continue;
134
+
135
+ int256 delta = _signedDelta(afterBalance, tokenState.beforeBalances[i]);
136
+ if (delta != 0) {
137
+ deltaTokensScratch[deltaCount] = candidates[i];
138
+ tokenDeltasScratch[deltaCount] = delta;
139
+ ++deltaCount;
140
+ }
141
+ }
142
+
143
+ result.deltaTokens = _trimAddresses(deltaTokensScratch, deltaCount);
144
+ result.tokenDeltas = _trimInts(tokenDeltasScratch, deltaCount);
145
+ }
146
+
147
+ function _executeCalls(
148
+ SimulatedCall[] calldata calls,
149
+ address[] calldata candidates,
150
+ AllowanceProbe[] calldata probes,
151
+ ExecutionState memory executionState,
152
+ uint256 nativeBefore
153
+ ) internal returns (bool success, uint256 failingCallIndex, bytes memory revertData, uint256 nativeMin) {
154
+ success = true;
155
+ failingCallIndex = type(uint256).max;
156
+ nativeMin = nativeBefore;
157
+ if (probes.length > 0) {
158
+ _recordAllowanceCheckpoints(probes, executionState.stride, 0, executionState.checkpoints);
159
+ }
160
+
161
+ for (uint256 i = 0; i < calls.length; ++i) {
162
+ (bool ok, bytes memory callRevertData) = _executeCall(calls[i]);
163
+ if (!ok) {
164
+ success = false;
165
+ failingCallIndex = i;
166
+ revertData = callRevertData;
167
+ if (probes.length > 0) {
168
+ _fillRemainingCheckpoints(probes.length, executionState.stride, i, executionState.checkpoints);
169
+ }
170
+ break;
171
+ }
172
+
173
+ uint256 nativeAfter = address(this).balance;
174
+ if (nativeAfter < nativeMin) nativeMin = nativeAfter;
175
+ _updateMinBalances(candidates, executionState.isToken, executionState.minBalances);
176
+ if (probes.length > 0) {
177
+ _recordAllowanceCheckpoints(probes, executionState.stride, i + 1, executionState.checkpoints);
178
+ }
179
+ }
180
+ }
181
+
182
+ function _executeCall(SimulatedCall calldata call_) internal returns (bool ok, bytes memory revertData) {
183
+ // forge-lint: disable-next-line(low-level-calls, arbitrary-send-eth, calls-loop)
184
+ return call_.to.call{value: call_.value}(call_.data);
185
+ }
186
+
187
+ function _updateMinBalances(address[] calldata candidates, bool[] memory isToken, uint256[] memory minBalances)
188
+ internal
189
+ view
190
+ {
191
+ for (uint256 i = 0; i < candidates.length; ++i) {
192
+ if (!isToken[i]) continue;
193
+
194
+ (bool ok, uint256 afterBalance) = _tryBalanceOf(candidates[i], address(this));
195
+ if (ok && afterBalance < minBalances[i]) minBalances[i] = afterBalance;
196
+ }
197
+ }
198
+
199
+ function _recordAllowanceCheckpoints(
200
+ AllowanceProbe[] calldata probes,
201
+ uint256 stride,
202
+ uint256 offset,
203
+ uint256[] memory checkpoints
204
+ ) internal view {
205
+ for (uint256 i = 0; i < probes.length; ++i) {
206
+ (bool ok, uint256 allowance) = _tryAllowance(probes[i].token, address(this), probes[i].spender);
207
+ checkpoints[i * stride + offset] = ok ? allowance : 0;
208
+ }
209
+ }
210
+
211
+ function _fillRemainingCheckpoints(
212
+ uint256 probeCount,
213
+ uint256 stride,
214
+ uint256 lastOffset,
215
+ uint256[] memory checkpoints
216
+ ) internal pure {
217
+ for (uint256 i = 0; i < probeCount; ++i) {
218
+ uint256 last = checkpoints[i * stride + lastOffset];
219
+ for (uint256 offset = lastOffset + 1; offset < stride; ++offset) {
220
+ checkpoints[i * stride + offset] = last;
221
+ }
222
+ }
223
+ }
224
+
225
+ function _tryBalanceOf(address token, address owner) internal view returns (bool ok, uint256 balance) {
226
+ // forge-lint: disable-next-line(low-level-calls, calls-loop)
227
+ (bool success, bytes memory data) = token.staticcall(abi.encodeWithSelector(BALANCE_OF_SELECTOR, owner));
228
+ if (!success || data.length < 32) return (ok, balance);
229
+ ok = success;
230
+ balance = abi.decode(data, (uint256));
231
+ }
232
+
233
+ function _tryAllowance(address token, address owner, address spender)
234
+ internal
235
+ view
236
+ returns (bool ok, uint256 allowance)
237
+ {
238
+ // forge-lint: disable-next-line(low-level-calls, calls-loop)
239
+ (bool success, bytes memory data) = token.staticcall(abi.encodeWithSelector(ALLOWANCE_SELECTOR, owner, spender));
240
+ if (!success || data.length < 32) return (ok, allowance);
241
+ ok = success;
242
+ allowance = abi.decode(data, (uint256));
243
+ }
244
+
245
+ function _signedDelta(uint256 afterBalance, uint256 beforeBalance) internal pure returns (int256) {
246
+ if (afterBalance >= beforeBalance) {
247
+ uint256 positiveDiff = afterBalance - beforeBalance;
248
+ if (positiveDiff > MAX_INT256) return type(int256).max;
249
+ // forge-lint: disable-next-line(unsafe-typecast)
250
+ return int256(positiveDiff);
251
+ }
252
+
253
+ uint256 negativeDiff = beforeBalance - afterBalance;
254
+ if (negativeDiff > MAX_INT256) return type(int256).min;
255
+ // forge-lint: disable-next-line(unsafe-typecast)
256
+ return -int256(negativeDiff);
257
+ }
258
+
259
+ function _trimAddresses(address[] memory input, uint256 length) internal pure returns (address[] memory output) {
260
+ output = new address[](length);
261
+ for (uint256 i = 0; i < length; ++i) {
262
+ output[i] = input[i];
263
+ }
264
+ }
265
+
266
+ function _trimInts(int256[] memory input, uint256 length) internal pure returns (int256[] memory output) {
267
+ output = new int256[](length);
268
+ for (uint256 i = 0; i < length; ++i) {
269
+ output[i] = input[i];
270
+ }
271
+ }
272
+
273
+ function _recover(bytes32 hash, bytes calldata signature) internal pure returns (address signer) {
274
+ if (signature.length == 65) {
275
+ bytes32 r;
276
+ bytes32 s;
277
+ uint8 v;
278
+ // forge-lint: disable-next-line(inline-assembly)
279
+ assembly {
280
+ r := calldataload(signature.offset)
281
+ s := calldataload(add(signature.offset, 0x20))
282
+ v := byte(0, calldataload(add(signature.offset, 0x40)))
283
+ }
284
+ if (v < 27) v += 27;
285
+ if (v != 27 && v != 28) return address(0);
286
+ return ecrecover(hash, v, r, s);
287
+ }
288
+
289
+ if (signature.length == 64) {
290
+ bytes32 r;
291
+ bytes32 vs;
292
+ // forge-lint: disable-next-line(inline-assembly)
293
+ assembly {
294
+ r := calldataload(signature.offset)
295
+ vs := calldataload(add(signature.offset, 0x20))
296
+ }
297
+ bytes32 s = bytes32(uint256(vs) & 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff);
298
+ // forge-lint: disable-next-line(unsafe-typecast)
299
+ uint8 v = uint8((uint256(vs) >> 255) + 27);
300
+ return ecrecover(hash, v, r, s);
301
+ }
302
+
303
+ return address(0);
304
+ }
305
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Default gas budget for simulator `eth_call` executions.
3
+ *
4
+ * The injected simulator changes gas accounting, so this is intentionally a generous execution
5
+ * budget rather than a gas estimate for the real transaction.
6
+ */
7
+ export declare const DEFAULT_SIMULATION_GAS_LIMIT = 16000000n;
8
+ /**
9
+ * Default forged token balance or allowance written by slot overrides.
10
+ *
11
+ * This is deliberately below `uint256.max`: standard ERC-20 implementations skip allowance
12
+ * decrements at exactly max allowance, which would hide required approvals during measurement.
13
+ */
14
+ export declare const OVERRIDE_TOKEN_AMOUNT: bigint;
15
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,4BAA4B,YAAc,CAAC;AAExD;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,QAAa,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Default gas budget for simulator `eth_call` executions.
3
+ *
4
+ * The injected simulator changes gas accounting, so this is intentionally a generous execution
5
+ * budget rather than a gas estimate for the real transaction.
6
+ */
7
+ export const DEFAULT_SIMULATION_GAS_LIMIT = 16000000n;
8
+ /**
9
+ * Default forged token balance or allowance written by slot overrides.
10
+ *
11
+ * This is deliberately below `uint256.max`: standard ERC-20 implementations skip allowance
12
+ * decrements at exactly max allowance, which would hide required approvals during measurement.
13
+ */
14
+ export const OVERRIDE_TOKEN_AMOUNT = 10n ** 50n;
15
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAAG,SAAW,CAAC;AAExD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAG,IAAI,GAAG,CAAC"}
@@ -0,0 +1,35 @@
1
+ /** Base class for typed infrastructure and input errors thrown by viem-tx-sim. */
2
+ export declare class TxSimError extends Error {
3
+ readonly name: string;
4
+ }
5
+ /**
6
+ * Thrown when the RPC endpoint cannot run `eth_createAccessList` for a non-transaction-revert reason.
7
+ *
8
+ * Transaction execution reverts during access-list creation are normalized to an empty access list;
9
+ * this error means provider capability or infrastructure failure. Try another RPC endpoint that
10
+ * supports EIP-2930 access lists for historical/state-overridden calls.
11
+ */
12
+ export declare class AccessListUnsupportedError extends TxSimError {
13
+ readonly name = "AccessListUnsupportedError";
14
+ constructor(message?: string);
15
+ }
16
+ /**
17
+ * Thrown when the RPC endpoint cannot execute `eth_call` with state overrides or returns bad output.
18
+ *
19
+ * This is usually a provider capability issue, including unsupported state overrides or simulator
20
+ * output that cannot be decoded. Retrying the same request generally will not help unless the RPC
21
+ * failure was transient; use a provider with state-override support.
22
+ */
23
+ export declare class StateOverrideUnsupportedError extends TxSimError {
24
+ readonly name = "StateOverrideUnsupportedError";
25
+ constructor(message?: string);
26
+ }
27
+ /**
28
+ * Thrown for caller-side input bugs, such as an empty call batch.
29
+ *
30
+ * This is not an RPC/provider issue and should be fixed before retrying.
31
+ */
32
+ export declare class InvalidSimulationInputError extends TxSimError {
33
+ readonly name = "InvalidSimulationInputError";
34
+ }
35
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,qBAAa,UAAW,SAAQ,KAAK;IACnC,SAAkB,IAAI,EAAE,MAAM,CAAgB;CAC/C;AAED;;;;;;GAMG;AACH,qBAAa,0BAA2B,SAAQ,UAAU;IACxD,SAAkB,IAAI,gCAAgC;gBAE1C,OAAO,SAA4E;CAGhG;AAED;;;;;;GAMG;AACH,qBAAa,6BAA8B,SAAQ,UAAU;IAC3D,SAAkB,IAAI,mCAAmC;gBAGvD,OAAO,SAAgF;CAI1F;AAED;;;;GAIG;AACH,qBAAa,2BAA4B,SAAQ,UAAU;IACzD,SAAkB,IAAI,iCAAiC;CACxD"}
package/dist/errors.js ADDED
@@ -0,0 +1,39 @@
1
+ /** Base class for typed infrastructure and input errors thrown by viem-tx-sim. */
2
+ export class TxSimError extends Error {
3
+ name = "TxSimError";
4
+ }
5
+ /**
6
+ * Thrown when the RPC endpoint cannot run `eth_createAccessList` for a non-transaction-revert reason.
7
+ *
8
+ * Transaction execution reverts during access-list creation are normalized to an empty access list;
9
+ * this error means provider capability or infrastructure failure. Try another RPC endpoint that
10
+ * supports EIP-2930 access lists for historical/state-overridden calls.
11
+ */
12
+ export class AccessListUnsupportedError extends TxSimError {
13
+ name = "AccessListUnsupportedError";
14
+ constructor(message = "RPC endpoint does not support eth_createAccessList for this simulation.") {
15
+ super(message);
16
+ }
17
+ }
18
+ /**
19
+ * Thrown when the RPC endpoint cannot execute `eth_call` with state overrides or returns bad output.
20
+ *
21
+ * This is usually a provider capability issue, including unsupported state overrides or simulator
22
+ * output that cannot be decoded. Retrying the same request generally will not help unless the RPC
23
+ * failure was transient; use a provider with state-override support.
24
+ */
25
+ export class StateOverrideUnsupportedError extends TxSimError {
26
+ name = "StateOverrideUnsupportedError";
27
+ constructor(message = "RPC endpoint does not support eth_call state overrides for this simulation.") {
28
+ super(message);
29
+ }
30
+ }
31
+ /**
32
+ * Thrown for caller-side input bugs, such as an empty call batch.
33
+ *
34
+ * This is not an RPC/provider issue and should be fixed before retrying.
35
+ */
36
+ export class InvalidSimulationInputError extends TxSimError {
37
+ name = "InvalidSimulationInputError";
38
+ }
39
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,MAAM,OAAO,UAAW,SAAQ,KAAK;IACjB,IAAI,GAAW,YAAY,CAAC;CAC/C;AAED;;;;;;GAMG;AACH,MAAM,OAAO,0BAA2B,SAAQ,UAAU;IACtC,IAAI,GAAG,4BAA4B,CAAC;IAEtD,YAAY,OAAO,GAAG,yEAAyE;QAC7F,KAAK,CAAC,OAAO,CAAC,CAAC;IACjB,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,OAAO,6BAA8B,SAAQ,UAAU;IACzC,IAAI,GAAG,+BAA+B,CAAC;IAEzD,YACE,OAAO,GAAG,6EAA6E;QAEvF,KAAK,CAAC,OAAO,CAAC,CAAC;IACjB,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,OAAO,2BAA4B,SAAQ,UAAU;IACvC,IAAI,GAAG,6BAA6B,CAAC;CACxD"}