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
@@ -0,0 +1,306 @@
1
+ import type { Abi, Address, Hex, StateOverride } from "viem";
2
+ import {
3
+ decodeErrorResult,
4
+ decodeFunctionResult,
5
+ encodeFunctionData,
6
+ parseAbi,
7
+ slice,
8
+ size,
9
+ } from "viem";
10
+
11
+ import type {
12
+ AssetBalanceDelta,
13
+ SimulatedCall,
14
+ SimulationResult,
15
+ TokenSlotOverride,
16
+ } from "../types.js";
17
+ import { InvalidSimulationInputError, StateOverrideUnsupportedError } from "../errors.js";
18
+ import { txSimulatorRuntimeBytecode } from "../generated/txSimulatorBytecode.js";
19
+ import {
20
+ MAX_UINT256,
21
+ addressKey,
22
+ getCallData,
23
+ normalizeAddress,
24
+ uint256Hex,
25
+ uniqueAddresses,
26
+ } from "./data.js";
27
+ import type { RpcCallArgs } from "./rpc.js";
28
+ import {
29
+ blockOptionsSpread,
30
+ buildCallParameters,
31
+ createAccessList,
32
+ formatRpcError,
33
+ withRpcDebug,
34
+ } from "./rpc.js";
35
+
36
+ type ProbeData = {
37
+ observedTokens: Address[];
38
+ candidates: Address[];
39
+ maxTokenOutflows: readonly bigint[];
40
+ maxNativeOutflow: bigint;
41
+ allowanceCheckpoints: readonly bigint[];
42
+ };
43
+
44
+ export type SimulatorResult = SimulationResult & {
45
+ probeData: ProbeData;
46
+ };
47
+
48
+ const txSimulatorAbi = parseAbi([
49
+ "struct SimulatedCall { address to; uint256 value; bytes data; }",
50
+ "struct AllowanceProbe { address token; address spender; }",
51
+ "struct SimulationResult { bool success; uint256 failingCallIndex; bytes revertData; int256 nativeDelta; address[] observedTokens; address[] deltaTokens; int256[] tokenDeltas; uint256[] maxTokenOutflows; uint256 maxNativeOutflow; uint256[] allowanceCheckpoints; }",
52
+ "function simulate(SimulatedCall[] calls, address[] candidates, AllowanceProbe[] probes) returns (SimulationResult)",
53
+ "function isValidSignature(bytes32 hash, bytes signature) view returns (bytes4)",
54
+ ]);
55
+
56
+ export async function runSimulator(
57
+ args: RpcCallArgs & {
58
+ from: Address;
59
+ calls: readonly SimulatedCall[];
60
+ candidates: readonly Address[];
61
+ tokenSlotOverrides?: readonly TokenSlotOverride[];
62
+ extraStateOverrides?: readonly StateOverrideEntry[];
63
+ allowanceProbes?: readonly { token: Address; spender: Address }[];
64
+ debugStep?: string;
65
+ errorAbi?: Abi;
66
+ },
67
+ ): Promise<SimulatorResult> {
68
+ const candidates = uniqueAddresses(args.candidates);
69
+ const data = encodeFunctionData({
70
+ abi: txSimulatorAbi,
71
+ functionName: "simulate",
72
+ args: [
73
+ args.calls.map((call) => ({
74
+ to: call.to,
75
+ value: call.value ?? 0n,
76
+ data: call.data,
77
+ })),
78
+ candidates,
79
+ args.allowanceProbes ?? [],
80
+ ],
81
+ });
82
+
83
+ const stateOverride = buildStateOverride([
84
+ { address: args.from, code: txSimulatorRuntimeBytecode },
85
+ ...tokenSlotOverridesToStateDiff(args.tokenSlotOverrides ?? []),
86
+ ...(args.extraStateOverrides ?? []),
87
+ ]);
88
+
89
+ let callData: Hex;
90
+ try {
91
+ const result = await withRpcDebug(
92
+ args.debug,
93
+ {
94
+ method: "eth_call",
95
+ step: args.debugStep ?? "txSimulator.simulate",
96
+ details: {
97
+ from: args.from,
98
+ calls: args.calls.length,
99
+ candidates: candidates.length,
100
+ storageOverrides: args.tokenSlotOverrides?.length ?? 0,
101
+ stateOverrideAccounts: stateOverride.length,
102
+ },
103
+ },
104
+ () =>
105
+ args.client.call(
106
+ buildCallParameters({
107
+ account: args.from,
108
+ to: args.from,
109
+ data,
110
+ gas: args.gas,
111
+ stateOverride,
112
+ ...blockOptionsSpread(args),
113
+ }),
114
+ ),
115
+ );
116
+ callData = getCallData(result);
117
+ } catch (cause) {
118
+ throw new StateOverrideUnsupportedError(
119
+ formatRpcError("eth_call with state override failed", cause),
120
+ );
121
+ }
122
+
123
+ let result;
124
+ try {
125
+ result = decodeFunctionResult({
126
+ abi: txSimulatorAbi,
127
+ functionName: "simulate",
128
+ data: callData,
129
+ });
130
+ } catch (cause) {
131
+ throw new StateOverrideUnsupportedError(
132
+ formatRpcError("eth_call returned undecodable simulator output", cause),
133
+ );
134
+ }
135
+
136
+ const assetBalanceDeltas: AssetBalanceDelta[] = [];
137
+ if (result.nativeDelta !== 0n) {
138
+ assetBalanceDeltas.push({ asset: "native", delta: result.nativeDelta });
139
+ }
140
+
141
+ for (let i = 0; i < result.deltaTokens.length; ++i) {
142
+ const token = result.deltaTokens[i];
143
+ const delta = result.tokenDeltas[i];
144
+ if (token && delta !== undefined && delta !== 0n) {
145
+ assetBalanceDeltas.push({ asset: token, delta });
146
+ }
147
+ }
148
+
149
+ const probeData = {
150
+ observedTokens: uniqueAddresses(result.observedTokens),
151
+ candidates,
152
+ maxTokenOutflows: result.maxTokenOutflows,
153
+ maxNativeOutflow: result.maxNativeOutflow,
154
+ allowanceCheckpoints: result.allowanceCheckpoints,
155
+ };
156
+
157
+ if (!result.success) {
158
+ const decodedRevert = decodeRevert(result.revertData, args.errorAbi);
159
+ return {
160
+ status: "reverted",
161
+ assetBalanceDeltas,
162
+ revertData: result.revertData,
163
+ ...(decodedRevert.revertReason !== undefined
164
+ ? { revertReason: decodedRevert.revertReason }
165
+ : {}),
166
+ ...(decodedRevert.revertError !== undefined
167
+ ? { revertError: decodedRevert.revertError }
168
+ : {}),
169
+ ...(decodedRevert.revertSelector !== undefined
170
+ ? { revertSelector: decodedRevert.revertSelector }
171
+ : {}),
172
+ failingCallIndex: Number(result.failingCallIndex),
173
+ probeData,
174
+ };
175
+ }
176
+
177
+ return {
178
+ status: "success",
179
+ assetBalanceDeltas,
180
+ probeData,
181
+ };
182
+ }
183
+
184
+ export async function discoverCandidateAddresses(
185
+ args: RpcCallArgs & {
186
+ from: Address;
187
+ calls: readonly SimulatedCall[];
188
+ },
189
+ ): Promise<Address[]> {
190
+ const accessLists = await Promise.all(
191
+ args.calls.map((call) =>
192
+ createAccessList({
193
+ client: args.client,
194
+ from: args.from,
195
+ to: call.to,
196
+ data: call.data,
197
+ value: call.value ?? 0n,
198
+ gas: args.gas,
199
+ debug: args.debug,
200
+ debugStep: "candidateDiscovery.accessList",
201
+ ...blockOptionsSpread(args),
202
+ }),
203
+ ),
204
+ );
205
+ const candidates = args.calls.flatMap((call, index) => [
206
+ call.to,
207
+ ...(accessLists[index] ?? []).map((entry) => entry.address),
208
+ ]);
209
+
210
+ return uniqueAddresses(candidates);
211
+ }
212
+
213
+ type DecodedRevert = {
214
+ revertReason?: string;
215
+ revertError?: { name: string; args: readonly unknown[] };
216
+ revertSelector?: Hex;
217
+ };
218
+
219
+ function decodeRevert(data: Hex | undefined, errorAbi?: Abi): DecodedRevert {
220
+ if (!data || data === "0x") return {};
221
+
222
+ const revertSelector = size(data) >= 4 ? slice(data, 0, 4) : undefined;
223
+ try {
224
+ const decoded = decodeErrorResult({ abi: errorAbi ?? [], data });
225
+ return {
226
+ revertReason: formatReason(decoded.errorName, decoded.args ?? []),
227
+ revertError: { name: decoded.errorName, args: decoded.args ?? [] },
228
+ ...(revertSelector !== undefined ? { revertSelector } : {}),
229
+ };
230
+ } catch {
231
+ return revertSelector !== undefined ? { revertSelector } : {};
232
+ }
233
+ }
234
+
235
+ function formatReason(name: string, args: readonly unknown[]): string {
236
+ if (name === "Error") return String(args[0]);
237
+ if (name === "Panic") return `Panic(${String(args[0])})`;
238
+ return `${name}(${args.map((arg) => String(arg)).join(", ")})`;
239
+ }
240
+
241
+ type StateOverrideEntry = StateOverride[number];
242
+
243
+ type MutableStateOverrideEntry = {
244
+ address: Address;
245
+ code?: Hex;
246
+ balance?: bigint;
247
+ stateDiff?: {
248
+ slot: Hex;
249
+ value: Hex;
250
+ }[];
251
+ };
252
+
253
+ function buildStateOverride(entries: readonly StateOverrideEntry[]): StateOverride {
254
+ const merged = new Map<string, MutableStateOverrideEntry>();
255
+
256
+ for (const entry of entries) {
257
+ const normalized = normalizeAddress(entry.address);
258
+ const key = addressKey(normalized);
259
+ const existing = merged.get(key) ?? { address: normalized, stateDiff: [] };
260
+
261
+ if (entry.code) existing.code = entry.code;
262
+ if (entry.balance !== undefined) existing.balance = entry.balance;
263
+ if (entry.stateDiff) {
264
+ const bySlot = new Map(
265
+ (existing.stateDiff ?? []).map((item) => [item.slot.toLowerCase(), item]),
266
+ );
267
+ for (const diff of entry.stateDiff) bySlot.set(diff.slot.toLowerCase(), diff);
268
+ existing.stateDiff = [...bySlot.values()];
269
+ }
270
+
271
+ merged.set(key, existing);
272
+ }
273
+
274
+ return [...merged.values()].map((entry): StateOverrideEntry => {
275
+ if (entry.stateDiff?.length === 0) {
276
+ return {
277
+ address: entry.address,
278
+ code: entry.code,
279
+ balance: entry.balance,
280
+ };
281
+ }
282
+ return entry;
283
+ });
284
+ }
285
+
286
+ function tokenSlotOverridesToStateDiff(overrides: readonly TokenSlotOverride[]): StateOverride {
287
+ const byAddress = new Map<string, MutableStateOverrideEntry>();
288
+
289
+ for (const override of overrides) {
290
+ if (override.amount === MAX_UINT256) {
291
+ throw new InvalidSimulationInputError(
292
+ "tokenSlotOverrides amount must be below uint256 max: max-allowance skips ERC-20 decrements and max-balance overflows incoming transfers. Use OVERRIDE_TOKEN_AMOUNT.",
293
+ );
294
+ }
295
+ const normalized = normalizeAddress(override.token);
296
+ const key = addressKey(normalized);
297
+ const entry = byAddress.get(key) ?? { address: normalized, stateDiff: [] };
298
+ entry.stateDiff?.push({
299
+ slot: override.slot,
300
+ value: uint256Hex(override.amount),
301
+ });
302
+ byAddress.set(key, entry);
303
+ }
304
+
305
+ return [...byAddress.values()];
306
+ }
@@ -0,0 +1,211 @@
1
+ import type { Address, Hex } from "viem";
2
+ import { encodeAbiParameters, keccak256 } from "viem";
3
+
4
+ import { OVERRIDE_TOKEN_AMOUNT } from "../constants.js";
5
+ import type {
6
+ PreparedAllowanceOverrides,
7
+ PreparedBalanceOverrides,
8
+ PrepareAllowanceOverridesArgs,
9
+ PrepareBalanceOverridesArgs,
10
+ TokenSlotOverride,
11
+ } from "../types.js";
12
+ import { addressKey, uint256Hex } from "./data.js";
13
+ import { discoverAllowanceSlot, discoverBalanceSlot, readAllowance } from "./probes.js";
14
+ import type { ClientArgs, RpcCallArgs } from "./rpc.js";
15
+ import { blockOptionsSpread } from "./rpc.js";
16
+
17
+ type SlotFact = {
18
+ token: Address;
19
+ slot: Hex;
20
+ };
21
+
22
+ type AllowanceSlotFact = SlotFact & {
23
+ spender: Address;
24
+ };
25
+
26
+ // Orchestration
27
+ /** @internal Implements `TxSimulator.prepareBalanceOverrides`. Prefer the instance API from the package root. */
28
+ export async function prepareBalanceOverrides(
29
+ args: PrepareBalanceOverridesArgs & ClientArgs,
30
+ ): Promise<PreparedBalanceOverrides> {
31
+ const slots = await Promise.all(
32
+ args.tokens.map((token) =>
33
+ discoverBalanceSlot({
34
+ client: args.client,
35
+ token,
36
+ owner: args.from,
37
+ sentinel: OVERRIDE_TOKEN_AMOUNT,
38
+ gas: args.gas,
39
+ debug: args.debug,
40
+ ...blockOptionsSpread(args),
41
+ }),
42
+ ),
43
+ );
44
+ return {
45
+ slots: slots.filter(isDefined).map(withOverrideAmount),
46
+ unresolved: args.tokens.filter((_, index) => slots[index] === undefined),
47
+ };
48
+ }
49
+
50
+ /** @internal Implements `TxSimulator.prepareAllowanceOverrides`. Prefer the instance API from the package root. */
51
+ export async function prepareAllowanceOverrides(
52
+ args: PrepareAllowanceOverridesArgs & ClientArgs,
53
+ ): Promise<PreparedAllowanceOverrides> {
54
+ const slots = await prepareAllowanceOverridesWithInference({
55
+ client: args.client,
56
+ from: args.from,
57
+ pairs: args.pairs,
58
+ sentinel: OVERRIDE_TOKEN_AMOUNT,
59
+ gas: args.gas,
60
+ debug: args.debug,
61
+ ...blockOptionsSpread(args),
62
+ });
63
+ return {
64
+ slots: slots.filter(isDefined).map(withOverrideAmount),
65
+ unresolved: args.pairs.filter((_, index) => slots[index] === undefined),
66
+ };
67
+ }
68
+
69
+ function isDefined<T>(value: T | undefined): value is T {
70
+ return value !== undefined;
71
+ }
72
+
73
+ function withOverrideAmount<T extends SlotFact>(slot: T): T & TokenSlotOverride {
74
+ return { ...slot, amount: OVERRIDE_TOKEN_AMOUNT };
75
+ }
76
+
77
+ // Inference internals
78
+ type AllowancePair = {
79
+ token: Address;
80
+ spender: Address;
81
+ };
82
+
83
+ type IndexedAllowancePair = AllowancePair & {
84
+ index: number;
85
+ };
86
+
87
+ async function prepareAllowanceOverridesWithInference(
88
+ args: RpcCallArgs & {
89
+ from: Address;
90
+ pairs: readonly AllowancePair[];
91
+ sentinel: bigint;
92
+ },
93
+ ): Promise<(AllowanceSlotFact | undefined)[]> {
94
+ const slots: (AllowanceSlotFact | undefined)[] = Array.from({ length: args.pairs.length });
95
+ const groups = groupPairsByToken(args.pairs);
96
+
97
+ await Promise.all(
98
+ groups.map(async (pairs) => {
99
+ const firstPair = pairs[0];
100
+ if (firstPair === undefined) return;
101
+
102
+ const firstSlot = await probeAllowanceSlot({ ...args, ...firstPair });
103
+ slots[firstPair.index] = firstSlot;
104
+ const baseSlot =
105
+ firstSlot === undefined
106
+ ? undefined
107
+ : inferAllowanceBaseSlot({
108
+ probedSlot: firstSlot.slot,
109
+ owner: args.from,
110
+ spender: firstPair.spender,
111
+ });
112
+
113
+ await Promise.all(
114
+ pairs.slice(1).map(async (pair) => {
115
+ slots[pair.index] =
116
+ baseSlot === undefined
117
+ ? await probeAllowanceSlot({ ...args, ...pair })
118
+ : await computeAllowanceSlot({ ...args, ...pair, baseSlot });
119
+ }),
120
+ );
121
+ }),
122
+ );
123
+
124
+ return slots;
125
+ }
126
+
127
+ function groupPairsByToken(pairs: readonly AllowancePair[]): IndexedAllowancePair[][] {
128
+ const groupsByToken = new Map<string, IndexedAllowancePair[]>();
129
+ for (let index = 0; index < pairs.length; ++index) {
130
+ const pair = pairs[index];
131
+ if (pair === undefined) continue;
132
+ const key = addressKey(pair.token);
133
+ const group = groupsByToken.get(key) ?? [];
134
+ group.push({ ...pair, index });
135
+ groupsByToken.set(key, group);
136
+ }
137
+ return [...groupsByToken.values()];
138
+ }
139
+
140
+ async function probeAllowanceSlot(
141
+ args: RpcCallArgs & {
142
+ from: Address;
143
+ token: Address;
144
+ spender: Address;
145
+ sentinel: bigint;
146
+ },
147
+ ): Promise<AllowanceSlotFact | undefined> {
148
+ return discoverAllowanceSlot({
149
+ client: args.client,
150
+ token: args.token,
151
+ owner: args.from,
152
+ spender: args.spender,
153
+ sentinel: args.sentinel,
154
+ gas: args.gas,
155
+ debug: args.debug,
156
+ ...blockOptionsSpread(args),
157
+ });
158
+ }
159
+
160
+ async function computeAllowanceSlot(
161
+ args: RpcCallArgs & {
162
+ from: Address;
163
+ token: Address;
164
+ spender: Address;
165
+ baseSlot: bigint;
166
+ sentinel: bigint;
167
+ },
168
+ ): Promise<AllowanceSlotFact | undefined> {
169
+ const slot = allowanceSlotFor(args.from, args.spender, args.baseSlot);
170
+ const allowance = await readAllowance({
171
+ client: args.client,
172
+ token: args.token,
173
+ owner: args.from,
174
+ spender: args.spender,
175
+ stateOverride: [
176
+ { address: args.token, stateDiff: [{ slot, value: uint256Hex(args.sentinel) }] },
177
+ ],
178
+ gas: args.gas,
179
+ debug: args.debug,
180
+ debugStep: "allowanceSlot.computedVerify",
181
+ ...blockOptionsSpread(args),
182
+ });
183
+ if (allowance === args.sentinel) return { token: args.token, spender: args.spender, slot };
184
+ return probeAllowanceSlot(args);
185
+ }
186
+
187
+ // Layout math
188
+ function mappingSlot(key: Address, baseSlot: Hex | bigint): Hex {
189
+ return keccak256(
190
+ encodeAbiParameters(
191
+ [{ type: "address" }, { type: "uint256" }],
192
+ [key, typeof baseSlot === "bigint" ? baseSlot : BigInt(baseSlot)],
193
+ ),
194
+ );
195
+ }
196
+
197
+ function allowanceSlotFor(owner: Address, spender: Address, base: bigint): Hex {
198
+ return mappingSlot(spender, mappingSlot(owner, base));
199
+ }
200
+
201
+ function inferAllowanceBaseSlot(args: {
202
+ probedSlot: Hex;
203
+ owner: Address;
204
+ spender: Address;
205
+ }): bigint | undefined {
206
+ const target = args.probedSlot.toLowerCase();
207
+ for (let base = 0n; base <= 64n; ++base) {
208
+ if (allowanceSlotFor(args.owner, args.spender, base).toLowerCase() === target) return base;
209
+ }
210
+ return undefined;
211
+ }
@@ -0,0 +1,176 @@
1
+ import { DEFAULT_SIMULATION_GAS_LIMIT } from "./constants.js";
2
+ import { InvalidSimulationInputError } from "./errors.js";
3
+ import { estimateAssetRequirements } from "./internal/requirements.js";
4
+ import { blockOptionsSpread, type ClientArgs } from "./internal/rpc.js";
5
+ import { discoverCandidateAddresses, runSimulator } from "./internal/simulator.js";
6
+ import { prepareAllowanceOverrides, prepareBalanceOverrides } from "./internal/slots.js";
7
+ import type {
8
+ PreparedAllowanceOverrides,
9
+ PreparedBalanceOverrides,
10
+ PrepareAllowanceOverridesArgs,
11
+ PrepareBalanceOverridesArgs,
12
+ EstimateAssetRequirementsArgs,
13
+ EstimatedAssetRequirements,
14
+ SimulateArgs,
15
+ SimulatedCall,
16
+ SimulationResult,
17
+ TxSimulatorConfig,
18
+ } from "./types.js";
19
+
20
+ type BoundCallDefaults = {
21
+ gas?: bigint;
22
+ debug?: TxSimulatorConfig["debug"];
23
+ errorAbi?: TxSimulatorConfig["errorAbi"];
24
+ };
25
+
26
+ /**
27
+ * Bound transaction simulator for one viem public client.
28
+ *
29
+ * Bind the RPC client once, then pass `from` per call so applications can switch accounts without
30
+ * rebuilding the simulator. Per-call `gas` and `debug` override defaults supplied to
31
+ * {@link TxSimulator.create}.
32
+ */
33
+ export interface TxSimulator {
34
+ /**
35
+ * Simulates one call or sequential batch and returns raw native/token balance deltas.
36
+ *
37
+ * This uses `eth_createAccessList` for candidate discovery and one `eth_call` with state
38
+ * overrides that injects the simulator at `from`. It does not automatically forge balances or
39
+ * allowances; preparation methods return ready-to-use `tokenSlotOverrides` for view-only or
40
+ * unfunded accounts. Transaction reverts return `status: "reverted"` instead of throwing.
41
+ *
42
+ * @throws InvalidSimulationInputError when `calls` is empty.
43
+ * @throws AccessListUnsupportedError when the RPC endpoint cannot provide access lists.
44
+ * @throws StateOverrideUnsupportedError when the RPC endpoint cannot execute state overrides or
45
+ * returns undecodable simulator output.
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * const sim = TxSimulator.create({ client });
50
+ * const result = await sim.simulate({
51
+ * from,
52
+ * calls: [{ to, data, value: 0n }],
53
+ * });
54
+ * ```
55
+ */
56
+ simulate: (args: SimulateArgs) => Promise<SimulationResult>;
57
+
58
+ /**
59
+ * Prepares ERC-20 balance overrides for `from`.
60
+ *
61
+ * Each token is probed with RPC-only access lists and sentinel state overrides. Tokens the
62
+ * simulator cannot `deal` by verified storage write are returned in `unresolved` rather than
63
+ * thrown.
64
+ *
65
+ * @throws StateOverrideUnsupportedError when the RPC endpoint cannot execute state overrides.
66
+ */
67
+ prepareBalanceOverrides: (args: PrepareBalanceOverridesArgs) => Promise<PreparedBalanceOverrides>;
68
+
69
+ /**
70
+ * Prepares ERC-20 allowance overrides for `from` and the requested token/spender pairs.
71
+ *
72
+ * Standard Solidity allowance layouts are inferred after one verified probe per token where
73
+ * possible; non-standard layouts fall back to per-pair probing. Pairs the simulator cannot `deal`
74
+ * via verified storage write are returned in `unresolved` rather than thrown.
75
+ *
76
+ * @throws StateOverrideUnsupportedError when the RPC endpoint cannot execute state overrides.
77
+ */
78
+ prepareAllowanceOverrides: (
79
+ args: PrepareAllowanceOverridesArgs,
80
+ ) => Promise<PreparedAllowanceOverrides>;
81
+
82
+ /**
83
+ * Estimates the balances and approvals needed to execute the observed path.
84
+ *
85
+ * Use this when the tokens or spenders are not known ahead of time. Returned amounts are estimated
86
+ * under forged balances/allowances and should be padded before display or transaction assembly;
87
+ * unreliable measurements are reported under `unresolved`.
88
+ *
89
+ * @throws InvalidSimulationInputError when `calls` is empty.
90
+ * @throws AccessListUnsupportedError when the RPC endpoint cannot provide access lists.
91
+ * @throws StateOverrideUnsupportedError when the RPC endpoint cannot execute state overrides or
92
+ * returns undecodable simulator output.
93
+ */
94
+ estimateAssetRequirements: (
95
+ args: EstimateAssetRequirementsArgs,
96
+ ) => Promise<EstimatedAssetRequirements>;
97
+ }
98
+
99
+ /** Factory for {@link TxSimulator} instances bound to one viem public client. */
100
+ export const TxSimulator = {
101
+ /**
102
+ * Creates a simulator with optional default gas and debug settings.
103
+ *
104
+ * `gas` defaults to `DEFAULT_SIMULATION_GAS_LIMIT`; `debug` may be `true` for console logging or a
105
+ * callback for structured events. Per-call `gas` and `debug` take precedence over these defaults.
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * const sim = TxSimulator.create({ client, debug: true });
110
+ * const result = await sim.simulate({ from, calls });
111
+ * ```
112
+ */
113
+ create(bound: TxSimulatorConfig): TxSimulator {
114
+ const defaults = (args: BoundCallDefaults) => {
115
+ const gas = args.gas ?? bound.gas ?? DEFAULT_SIMULATION_GAS_LIMIT;
116
+ const debug = args.debug ?? bound.debug;
117
+
118
+ return {
119
+ gas,
120
+ ...(debug !== undefined ? { debug } : {}),
121
+ };
122
+ };
123
+ const revertDefaults = (args: BoundCallDefaults) => {
124
+ const errorAbi = [...(bound.errorAbi ?? []), ...(args.errorAbi ?? [])];
125
+
126
+ return {
127
+ ...defaults(args),
128
+ ...(errorAbi.length > 0 ? { errorAbi } : {}),
129
+ };
130
+ };
131
+
132
+ return {
133
+ simulate: (args) => runSimulate({ ...args, ...revertDefaults(args), client: bound.client }),
134
+ prepareBalanceOverrides: (args) =>
135
+ prepareBalanceOverrides({ ...args, ...defaults(args), client: bound.client }),
136
+ prepareAllowanceOverrides: (args) =>
137
+ prepareAllowanceOverrides({ ...args, ...defaults(args), client: bound.client }),
138
+ estimateAssetRequirements: (args) =>
139
+ estimateAssetRequirements({ ...args, ...revertDefaults(args), client: bound.client }),
140
+ };
141
+ },
142
+ };
143
+
144
+ async function runSimulate(args: SimulateArgs & ClientArgs): Promise<SimulationResult> {
145
+ if (args.calls.length === 0) {
146
+ throw new InvalidSimulationInputError("simulate requires at least one call.");
147
+ }
148
+
149
+ const calls = args.calls.map((call) => ({
150
+ to: call.to,
151
+ data: call.data,
152
+ value: call.value ?? 0n,
153
+ })) satisfies SimulatedCall[];
154
+ const candidateAddresses = await discoverCandidateAddresses({
155
+ client: args.client,
156
+ from: args.from,
157
+ calls,
158
+ ...blockOptionsSpread(args),
159
+ gas: args.gas,
160
+ ...(args.debug !== undefined ? { debug: args.debug } : {}),
161
+ });
162
+
163
+ const tokenSlotOverrides = args.tokenSlotOverrides ?? [];
164
+
165
+ return runSimulator({
166
+ client: args.client,
167
+ from: args.from,
168
+ calls,
169
+ candidates: [...candidateAddresses, ...tokenSlotOverrides.map((slot) => slot.token)],
170
+ tokenSlotOverrides,
171
+ debug: args.debug,
172
+ ...blockOptionsSpread(args),
173
+ gas: args.gas,
174
+ ...(args.errorAbi !== undefined ? { errorAbi: args.errorAbi } : {}),
175
+ });
176
+ }